ERC721 NFT
A complete example of an ERC721 (NFT) implementation using the AssemblyScript Stylus SDK.
Complete Contract: You can view the full ERC721 contract implementation here.
Missing Methods
Note: The safeTransferFrom
and safeMint
methods are not currently implemented in this example. These methods will be added in future updates to provide full ERC721 compliance with receiver callbacks.
Contract Structure
Custom Errors
Specific errors for different validations:
@Error
class ERC721InvalidOwner {
owner: Address;
}
@Error
class ERC721NonexistentToken {
tokenId: U256;
}
@Error
class ERC721IncorrectOwner {
sender: Address;
tokenId: U256;
owner: Address;
}
@Error
class ERC721InsufficientApproval {
sender: Address;
tokenId: U256;
}
Events
Events to track transfers and approvals:
@Event
export class Transfer {
@Indexed from: Address;
@Indexed to: Address;
@Indexed tokenId: U256;
}
@Event
export class Approval {
@Indexed owner: Address;
@Indexed spender: Address;
@Indexed tokenId: U256;
}
@Event
export class ApprovalForAll {
@Indexed owner: Address;
@Indexed operator: Address;
approved: boolean;
}
Storage State
Mappings to manage ownership and approvals:
@Contract
export class ERC721 {
static owners: Mapping<U256, Address> = new Mapping<U256, Address>();
static balances: Mapping<Address, U256> = new Mapping<Address, U256>();
static tokenApprovals: Mapping<U256, Address> = new Mapping<U256, Address>();
static operatorApprovals: Mapping2<Address, Address, boolean> = new Mapping2<Address, Address, boolean>();
static name: Str;
static symbol: Str;
Constructor
Initialize the NFT collection:
constructor(_name: string, _symbol: string) {
const nameStr = StrFactory.fromString(_name);
const symbolStr = StrFactory.fromString(_symbol);
name = nameStr;
symbol = symbolStr;
}
Approval Functions
Permission system for transferring NFTs:
@External
static approve(to: Address, tokenId: U256): void {
const authorizer = msg.sender;
const owner = owners.get(tokenId);
if (owner.isZero()) {
ERC721NonexistentToken.revert(tokenId);
}
const isOwnerAuth = owner.equals(authorizer);
const isApprovedForAll = operatorApprovals.get(owner, authorizer);
const isAuthorized = isOwnerAuth || isApprovedForAll;
if (!isAuthorized) {
ERC721InvalidApprover.revert(authorizer);
}
tokenApprovals.set(tokenId, to);
Approval.emit(owner, to, tokenId);
}
@External
static setApprovalForAll(operator: Address, approved: boolean): void {
if (operator.isZero()) {
ERC721InvalidOperator.revert(operator);
}
const owner = msg.sender;
operatorApprovals.set(owner, operator, approved);
ApprovalForAll.emit(owner, operator, approved);
}
Transfer Function
Transfer NFTs between accounts with full validations:
@External
static transferFrom(from: Address, to: Address, tokenId: U256): void {
const zeroAddress = AddressFactory.fromString("0x0000000000000000000000000000000000000000");
const one = U256Factory.fromString("1");
if (to.isZero()) {
ERC721InvalidReceiver.revert(to);
}
const owner = owners.get(tokenId);
const authorizer = msg.sender;
const isOwnerZero = owner.isZero();
const approvedAddress = tokenApprovals.get(tokenId);
const isApprovedForAll = operatorApprovals.get(owner, authorizer);
const isAuthOwner = authorizer.equals(owner);
const isAuthApproved = authorizer.equals(approvedAddress);
const isAuthorized = isAuthOwner || isAuthApproved || isApprovedForAll;
if (!isAuthorized) {
if (isOwnerZero) {
ERC721NonexistentToken.revert(tokenId);
} else {
ERC721InsufficientApproval.revert(authorizer, tokenId);
}
}
if (!owner.equals(from)) {
ERC721IncorrectOwner.revert(authorizer, tokenId, owner);
}
tokenApprovals.set(tokenId, zeroAddress);
const fromBalance = balances.get(owner);
balances.set(owner, fromBalance.sub(one));
const toBalance = balances.get(to);
balances.set(to, toBalance.add(one));
owners.set(tokenId, to);
Transfer.emit(owner, to, tokenId);
}
Mint & Burn
Functions to create and destroy NFTs:
@External
static mint(to: Address, tokenId: U256): void {
const zeroAddress = AddressFactory.fromString("0x0000000000000000000000000000000000000000");
const one = U256Factory.fromString("1");
if (to.isZero()) {
ERC721InvalidReceiver.revert(zeroAddress);
}
const from = owners.get(tokenId);
if (!from.isZero()) {
ERC721InvalidSender.revert(zeroAddress);
}
const toBalance = balances.get(to);
balances.set(to, toBalance.add(one));
owners.set(tokenId, to);
Transfer.emit(from, to, tokenId);
}
@External
static burn(tokenId: U256): void {
const zeroAddress = AddressFactory.fromString("0x0000000000000000000000000000000000000000");
const one = U256Factory.fromString("1");
const from = owners.get(tokenId);
if (from.isZero()) {
ERC721NonexistentToken.revert(tokenId);
}
tokenApprovals.set(tokenId, zeroAddress);
const fromBalance = balances.get(from);
balances.set(from, fromBalance.sub(one));
owners.set(tokenId, zeroAddress);
Transfer.emit(from, zeroAddress, tokenId);
}
View Functions
Query methods for NFT information:
@View
static balanceOf(owner: Address): U256 {
if (owner.isZero()) {
ERC721InvalidOwner.revert(owner);
}
return balances.get(owner);
}
@View
static ownerOf(tokenId: U256): Address {
const owner = owners.get(tokenId);
if (owner.isZero()) {
ERC721NonexistentToken.revert(tokenId);
}
return owner;
}
@View
static name(): string {
return name;
}
@View
static symbol(): string {
return symbol;
}
@View
static getApproved(tokenId: U256): Address {
const owner = owners.get(tokenId);
if (owner.isZero()) {
ERC721NonexistentToken.revert(tokenId);
}
return tokenApprovals.get(tokenId);
}
@View
static isApprovedForAll(owner: Address, operator: Address): boolean {
return operatorApprovals.get(owner, operator);
}
}