Skip to content

Instantly share code, notes, and snippets.

@kprimice
Last active July 18, 2021 02:48
Show Gist options
  • Save kprimice/6801efca7acbfe4890b0fbd73e525381 to your computer and use it in GitHub Desktop.
Save kprimice/6801efca7acbfe4890b0fbd73e525381 to your computer and use it in GitHub Desktop.
This contract includes: PST (default token), multiple NFTs handling, NFT profit sharing (royalties), multiple editions NFTs (no.), third party approval, batch transfers, multiple contract owners (two levels) and foreignInvoke
declare function ContractAssert(cond: any, e: string): asserts cond;
declare function ContractError(e: string): void;
import { SmartWeaveGlobal } from "smartweave/lib/smartweave-global";
declare const SmartWeave: SmartWeaveGlobal;
const PST = "PTY"
const UNITY = 1e12
const ERR_404TOKENID = "No token found: Invalid tokenId"
const ERR_NOTOKENID = "No tokenId specified"
const ERR_NOQTY = "No qty specified"
const ERR_NOTARGET = "No target specified"
const ERR_NOROYALTIES = "No royalties specified"
const ERR_NOFROM = "No sender specified"
const ERR_INVALID = "Invalid token transfer"
const ERR_INTEGER = "Invalid value. Must be an integer"
type Token = {
ticker: string,
owners?: string[], // Ordered list of NFTs' owners
balances: Record<string, number>, // owner -> balance
royalties?: Record<string, number>, // TODO: facultative?
}
type numberSettings = "primaryRate" | "secondaryRate" | "royaltyRate";
type boolSettings = "allowFreeTransfer" | "paused" ;
type stringSettings = "communityChest";
type stringsSettings = "contractOwners" | "contractSuperOwners" | "auctions";
type Settings = { [key in numberSettings]: number} &
{ [key in boolSettings]: boolean} &
{ [key in stringSettings]: string} &
{ [key in stringsSettings]: string[]};
type State = {
nonce: number,
settings: Settings,
tokens: Record<string, Token>, // token ID -> Token
operatorApprovals: Record<string, Record<string, boolean>> // owner -> approved operators...
invocations: string[],
};
type Action = {
input: {
function: string,
settings: Settings,
from?: string,
froms?: string[],
target?: string,
targets?: string[],
royalties?: Record<string, number>,
owner?: string,
tokenId?: string,
tokenIds?: string[],
qty?: number,
qtys?: number[],
price?: number,
prices?: number[],
approved?: boolean,
no?: number,
nos?: number[]
invocationId?: string,
},
caller: string,
};
function tickerOf(state: State, tokenId: string): string {
const token = state.tokens[tokenId]
ContractAssert(token, ERR_404TOKENID)
const ticker = token.ticker
return ticker
}
function balanceOf(state: State, tokenId: string, target: string): number {
const token = state.tokens[tokenId]
ContractAssert(token, ERR_404TOKENID)
let qty = token.balances[target]
if (!qty) {
qty = 0
}
return qty
}
function royaltiesOf(state: State, tokenId: string, target: string): number {
const token = state.tokens[tokenId]
ContractAssert(token, ERR_404TOKENID)
let qty = token.royalties ? token.royalties[target] : 0
if (!qty) {
qty = 0
}
return qty
}
function mintToken(state: State, target: string, tokenId: string, royalties?: Record<string, number>, qty?: number, no?:number) {
ContractAssert(!(tokenId in state.tokens), `tokenId already exists: "${tokenId}".`)
if (royalties) {
const sum = Object.values(royalties).reduce((acc, val) => acc + val, 0);
ContractAssert(sum === UNITY, `Sum of royalties shares must be ${UNITY}`)
}
let token = <Token>{
ticker: `${PST}${state.nonce}`,
royalties: royalties,
owners: undefined,
balances: {},
}
state.nonce++
state.tokens[tokenId] = token
if (no) { // is an NFT
ContractAssert(Number.isInteger(no), ERR_INTEGER)
token.owners = Array(no).fill(target)
addTokenTo(state, target, tokenId, no)
} else if (qty) { // is a token
ContractAssert(!no, "no. must not be specified for tokens")
addTokenTo(state, target, tokenId, qty)
}
}
function addRoyaltiesTo(token: Token, target: string, qty: number) {
ContractAssert(token.royalties, ERR_NOROYALTIES);
if (!(target in token.royalties)) {
token.royalties[target] = 0
}
token.royalties[target] = token.royalties[target] + qty
}
function removeRoyaltiesFrom(token: Token, from: string, qty: number) {
ContractAssert(token.royalties, ERR_NOROYALTIES);
const fromRoyalties = token.royalties[from] || 0
ContractAssert(fromRoyalties > 0, "Sender does not own royalties on the token");
ContractAssert(fromRoyalties >= qty, "Insufficient royalties' balance");
const newBalance = token.royalties[from] - qty;
if (newBalance == 0) {
delete token.royalties[from]
} else {
token.royalties[from] = newBalance
}
}
function pay(state: State, token: Token, from: string, price: number) {
ContractAssert(token.royalties, ERR_NOROYALTIES);
ContractAssert(Number.isInteger(price), ERR_INTEGER)
ContractAssert(price >= 0, "Invalid value for price. Must be positive")
if (price == 0) {
return
}
if (from in token.royalties) { // primary sales
addTokenTo(state, state.settings["communityChest"], PST, price * state.settings["primaryRate"])
for (const [target, split] of Object.entries(token.royalties)) {
addTokenTo(state, target, PST, price * (1 - state.settings["primaryRate"]) * split / UNITY)
}
} else { // secondary sales
addTokenTo(state, state.settings["communityChest"], PST, price * state.settings["secondaryRate"])
for (const [target, split] of Object.entries(token.royalties)) {
addTokenTo(state, target, PST, price * state.settings["royaltyRate"] * split / UNITY)
}
}
}
function addTokenTo(state: State, target: string, tokenId: string, qty: number, no?:number) {
ContractAssert(Number.isInteger(qty), ERR_INTEGER)
ContractAssert(qty >= 0, "Invalid value for qty. Must be positive")
if (qty == 0) return
let token = state.tokens[tokenId]
ContractAssert(token, "tokenId does not exist")
if (!(target in token.balances)) {
token.balances[target] = 0
}
token.balances[target] = token.balances[target] + qty
if (token.owners && no) {
ContractAssert(Number.isInteger(no), ERR_INTEGER)
ContractAssert(qty === 1, "Amount must be 1 for NFTs")
ContractAssert(token.owners[no-1] === "", "Token no. is already attributed")
token.owners[no-1] = target
}
}
function removeTokenFrom(state: State, from: string, tokenId: string, qty: number, no?:number) {
const fromBalance = balanceOf(state, tokenId, from)
ContractAssert(fromBalance > 0, "Sender does not own the token");
ContractAssert(fromBalance >= qty, "Insufficient balance");
let token = state.tokens[tokenId]
const newBalance = token.balances[from] - qty;
if (token.owners && no) {
ContractAssert(Number.isInteger(no), ERR_INTEGER)
ContractAssert(no > 0, "Invalid value for no. Must be positive")
ContractAssert(qty === 1, "Amount must be 1 for NFTs")
// ContractAssert(newBalance === 0, "NewBalance must be 0")
ContractAssert(token.owners[no-1] === from, "Token no. is not owned by caller")
token.owners[no-1] = ""
}
if (newBalance == 0) {
delete token.balances[from]
} else {
token.balances[from] = newBalance
}
}
function ownersOf(state: State, tokenId: string): string[] {
const token = state.tokens[tokenId]
ContractAssert(token, ERR_404TOKENID);
return Object.keys(token.balances)
}
// Is caller allowed to move owners tokens?
function isApprovedForAll(state: State, target: string, caller: string): boolean {
if (!(target in state.operatorApprovals))
return false;
if (!(caller in state.operatorApprovals[target]))
return false;
return state.operatorApprovals[target][caller];
}
function isApprovedOrOwner(state: State, caller: string, target: string, tokenId: string, no?: number): boolean {
const token = state.tokens[tokenId]
ContractAssert(token, ERR_404TOKENID);
ContractAssert(!token.owners || no, "no. must be set to look for NFT owner");
if (token.owners && no && token.owners[no-1] === caller) {
return true
} else if (!token.owners && token.balances[caller]) {
return true
}
return isApprovedForAll(state, target, caller)
}
export async function handle(state: State, action: Action): Promise<{state?: State, result?:unknown}> {
const input = action.input
const caller = action.caller
const paused = state.settings["paused"]
const contractOwners = state.settings["contractOwners"]
const contractSuperOwners = state.settings["contractSuperOwners"]
// ContractAssert(input.function, "No function specified")
if (input.function === 'name') {
return { result : { name: "Pianity" } }
}
if (input.function === 'ticker') {
const tokenId = input.tokenId || PST;
const ticker = tickerOf(state, tokenId)
return { result: { ticker } }
}
if (input.function === 'balance') {
const target = input.target || caller;
const tokenId = input.tokenId || PST;
const balance = balanceOf(state, tokenId, target)
return { result: { target, balance } }
}
if (input.function === 'royalties') {
const target = input.target;
const tokenId = input.tokenId;
ContractAssert(tokenId, ERR_NOTOKENID)
ContractAssert(target, ERR_NOTARGET)
const royalties = royaltiesOf(state, tokenId, target)
return { result: { royalties } }
}
if (input.function === 'owner' || input.function == 'owners') {
const tokenId = input.tokenId
ContractAssert(tokenId, ERR_NOTOKENID)
const owners = ownersOf(state, tokenId)
return { result: { owners } }
}
if (input.function === 'isApprovedForAll') {
const target = input.target
const owner = input.owner
ContractAssert(owner, 'No owner specified');
ContractAssert(target, ERR_NOTARGET);
const approved = isApprovedForAll(state, target, owner)
return { result: { approved } }
}
ContractAssert(!paused || contractSuperOwners.includes(caller), "The contract must not be paused")
if (input.function === 'setApprovalForAll') {
const approved = input.approved
const target = input.target
ContractAssert(target, ERR_NOTARGET);
ContractAssert(typeof approved !== 'undefined', 'No approved parameter specified');
ContractAssert(target !== caller, 'Target must be different from the caller');
if (!(caller in state.operatorApprovals)) {
state.operatorApprovals[caller] = {}
}
state.operatorApprovals[caller][target] = approved
return { state }
}
if (input.function === 'transferBatch') {
const targets = input.targets;
const froms = input.froms;
const tokenIds = input.tokenIds;
const qtys = input.qtys;
const prices = input.prices;
const nos = input.nos;
ContractAssert(froms, ERR_NOFROM)
ContractAssert(tokenIds, ERR_NOTOKENID)
ContractAssert(targets, ERR_NOTARGET)
ContractAssert(tokenIds.length === froms.length, "tokenIds and froms length mismatch")
ContractAssert(tokenIds.length === targets.length, "tokenIds and targets length mismatch")
for (var i in tokenIds) {
ContractAssert(froms[i] !== targets[i], ERR_INVALID)
let no = nos? nos[i] : undefined
let qty = qtys? qtys[i] : undefined
let price = prices? prices[i] : undefined
const token = state.tokens[tokenIds[i]]
ContractAssert(token, ERR_404TOKENID)
ContractAssert(!token.owners || (no && !qty), "no. must be set and qty unset for NFTs")
ContractAssert(token.owners || (!no && qty), "qty must be set and no unset for tokens")
ContractAssert(isApprovedOrOwner(state, caller, froms[i], tokenIds[i], no), `Sender is not the owner of the token (input ${i})`)
if (token.royalties) {
ContractAssert(state.settings["allowFreeTransfer"] || contractOwners.includes(caller), "Free transfers not allowed")
ContractAssert(!price || contractOwners.includes(caller), "Priced transfers must come from contract owner")
removeTokenFrom(state, targets[i], PST, price || 0)
pay(state, token, froms[i], price || 0)
}
removeTokenFrom(state, froms[i], tokenIds[i], qty || 1, no)
addTokenTo(state, targets[i], tokenIds[i], qty || 1, no)
}
return { state }
}
if (input.function === 'transfer') {
const target = input.target;
const from = input.from || caller;
const tokenId = input.tokenId || PST;
const qty = input.qty;
const price = input.price;
const no = input.no;
ContractAssert(target, ERR_NOTARGET)
ContractAssert(from !== target, ERR_INVALID)
const token = state.tokens[tokenId]
ContractAssert(token, ERR_404TOKENID)
ContractAssert(!token.owners || (no && !qty), "no. must be set and qty unset for NFTs")
ContractAssert(token.owners || (!no && qty), "qty must be set and no unset for tokens")
ContractAssert(isApprovedOrOwner(state, caller, from, tokenId, no), "Sender is not approved nor the owner of the token")
if (token.royalties) {
ContractAssert(state.settings["allowFreeTransfer"] || contractOwners.includes(caller), "Free transfers not allowed")
ContractAssert(!price || contractOwners.includes(caller), "Priced transfers must come from contract owner")
removeTokenFrom(state, target, PST, price || 0)
pay(state, token, from, price || 0)
}
removeTokenFrom(state, from, tokenId, qty || 1, no)
addTokenTo(state, target, tokenId, qty || 1, no)
return { state }
}
if (input.function === 'transferRoyalties') {
const target = input.target;
const tokenId = input.tokenId;
const qty = input.qty;
ContractAssert(target, ERR_NOTARGET)
ContractAssert(qty, ERR_NOQTY)
ContractAssert(caller !== target, ERR_INVALID)
ContractAssert(tokenId, ERR_NOTOKENID)
ContractAssert(qty > 0, "Invalid value for qty. Must be positive")
let token = state.tokens[tokenId];
ContractAssert(token, "tokenId does not exist");
ContractAssert(token.royalties, "Royalties are not set for this token")
removeRoyaltiesFrom(token, caller, qty);
addRoyaltiesTo(token, target, qty);
const sum = Object.values(token.royalties).reduce((acc, val) => acc + val, 0);
ContractAssert(sum === UNITY, `Sum of royalties shares must be ${UNITY}`)
return { state }
}
if (input.function === "foreignInvoke") {
const target = input.target
const invocationId = input.invocationId
ContractAssert(contractOwners.includes(caller), 'Caller is not authorized to foreignInvoke');
ContractAssert(target, ERR_NOTARGET);
ContractAssert(typeof invocationId !== 'undefined', "No invocationId specified");
ContractAssert(state.settings["auctions"], "No auctions specified");
ContractAssert(state.settings["auctions"].includes(target), "Invalid auction contract");
const foreignState = await SmartWeave.contracts.readContractState(target);
ContractAssert(foreignState.foreignCalls, "Contract is missing support for foreign calls");
const invocation = foreignState.foreignCalls[invocationId];
ContractAssert(invocation, `Incorrect invocationId: invocation not found (${invocationId})`)
ContractAssert(state.invocations.includes(invocation), "Contract invocation already exists")
const foreignAction = action;
foreignAction.input = invocation;
const resultState = await handle(state, foreignAction);
return { state: resultState.state };
}
if (input.function === 'mint') {
const target = input.target
const tokenId = SmartWeave.transaction.id
const royalties = input.royalties
const qty = input.qty
const no = input.no
ContractAssert(contractOwners.includes(caller), 'Caller is not authorized to mint');
ContractAssert(target, ERR_NOTARGET);
ContractAssert(tokenId, ERR_NOTOKENID);
ContractAssert((qty && !no) || (!qty && no), "qty and no can't be set simultaneously")
mintToken(state, target, tokenId, royalties, qty, no)
return { state }
}
if (input.function === 'settings') {
const settings = input.settings
const contractSuperOwners = state.settings["contractSuperOwners"]
const keys = Object.keys(settings)
ContractAssert(settings, "No settings specified")
if (!contractSuperOwners.includes(caller)) {
ContractAssert(contractOwners.includes(caller), 'Caller is not authorized to edit contract settings');
ContractAssert(!keys.includes("paused") || settings["paused"] === true, "Caller is not Super Owner")
ContractAssert(!keys.includes("contractOwners"), "Caller is not Super Owner")
ContractAssert(!keys.includes("contractSuperOwners"), "Caller is not Super Owner")
}
Object.assign(state.settings, settings);
ContractAssert(state.settings["contractSuperOwners"].length > 0, "Can't delete all the Super Owners");
return { state }
}
throw new (ContractError as any)(`No function supplied or function not recognised: "${input.function}".`);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment