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 arc-72 example #1556

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
165 changes: 165 additions & 0 deletions examples/arc-72/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { loadStdlib } from '@reach-sh/stdlib';
import * as backend from './build/index.main.mjs';
const stdlib = loadStdlib(process.env);
const assert = stdlib.assert;

const assertFail = async (promise, errStr) => {
try {
await promise;
} catch (e) {
if (errStr) {
if (`${e}`.includes(errStr)) {
return;
}
throw `Expected exception including message: "${errStr}", but got: ${e}`;
} else {
return;
}
}
throw `Expected exception but did not catch one: ${errStr}`;
};
const assertEq = (a, b, context = 'assertEq') => {
if (a === b) return;
try {
const res1BN = bigNumberify(a);
const res2BN = bigNumberify(b);
if (res1BN.eq(res2BN)) return;
} catch {}
try {
const stripNulls = (s) => s.replace(/\0*$/g, "");
if (stripNulls(`${a}`) === stripNulls(`${b}`)) return;
} catch {}
try {
if (JSON.stringify(a) === JSON.stringify(b)) return;
} catch {}
try {
if (parseInt(a) == parseInt(b)) return;
} catch {}
assert(false, `${context}: ${a} == ${b}`);
};

const initBal = stdlib.parseCurrency(100);
const accs = await stdlib.newTestAccounts(4, initBal);
accs.forEach(acc => acc.setGasLimit(5000000));
const [acc0, acc1, acc2, acc3] = accs;
const [addr0, addr1, addr2, addr3] = accs.map(a => a.getAddress());

const zeroAddress = "0x" + "0".repeat(40);

const ctc0 = acc0.contract(backend);
const params = {
zeroAddress,
// This is the length of an ipfs base32 cid v1
metadataUriBase: "ipfs://bafy0000000000000000000000000000000000000000000000000000000/",
};
console.log("About to launch NFT contract.");
await stdlib.withDisconnect(() => ctc0.participants.Deployer({
params,
ready: () => {stdlib.disconnect()}
}));
const ctcinfo = await ctc0.getInfo();
const ctc = acc => acc.contract(backend, ctcinfo);
const [ctc1, ctc2, ctc3] = [acc1, acc2, acc3].map(a => ctc(a));

const assertEvent = async (event, ...expectedArgs) => {
const e = await ctc0.events[event].next();
const actualArgs = e.what;
expectedArgs.forEach((expectedArg, i) =>
assertEq(actualArgs[i], expectedArg, `${event} field ${i}`)
);
};

const assertView = async (view, args, expectedRet) => {
const ret = await ctc0.unsafeViews[view](...args);
assertEq(
ret,
expectedRet,
`view ${view}: expected ${expectedRet}, got ${ret}`
);
};

////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// Test happy/sad paths

console.log("About to start testing.");
// Interface ID that must be false
await assertView("supportsInterface", ["0xffffffff"], false);
// supportsInterface (ARC-73) interface ID
await assertView("supportsInterface", ["0x4e22a3ba"], true);
// ARC-72 core id
await assertView("supportsInterface", ["0x15974096"], true);
// ARC-72 metadata id
await assertView("supportsInterface", ["0x9112544c"], true);
// ARC-72 transfer management id
await assertView("supportsInterface", ["0x924d64fb"], true);
// Some non-existent interface ID
await assertView("supportsInterface", ["0x12345678"], false);

await assertView("ownerOf", [1], zeroAddress);
await assertFail(ctc1.a.mintTo(addr2), "must be admin");
await assertFail(ctc2.a.transferFrom(addr2, addr3, 1), "nft must exist");

console.log("About to mint first NFT.");
await ctc0.a.mintTo(addr2);
await assertEvent("Transfer", zeroAddress, addr2, 1);
await assertView("ownerOf", [1], addr2)
await assertView("tokenURI", [1], params.metadataUriBase + "0001");
await assertFail(ctc0.a.transferFrom(addr2, addr3, 1), "must be nft owner or approved operator");
await assertFail(ctc0.a.transferFrom(addr0, addr3, 1), "owner specified in API must be correct");
await ctc2.a.transferFrom(addr2, addr3, 1);
await assertEvent("Transfer", addr2, addr3, 1);
await assertView("ownerOf", [1], addr3)
await assertFail(ctc0.a.transferFrom(addr3, addr1, 1), "must be nft owner or approved operator");
await assertFail(ctc0.a.transferFrom(addr0, addr1, 1), "owner specified in API must be correct");
await ctc3.a.transferFrom(addr3, addr2, 1);
await assertEvent("Transfer", addr3, addr2, 1);
await assertView("ownerOf", [1], addr2)

console.log("About to test transfer management.");
await ctc0.a.mintTo(addr2);
await assertEvent("Transfer", zeroAddress, addr2, 2);
await assertView("ownerOf", [2], addr2)
await assertView("tokenURI", [2], params.metadataUriBase + "0002");
await assertView("getApproved", [2], zeroAddress);
await assertView("isApprovedForAll", [addr3, addr1], false);
await ctc3.a.setApprovalForAll(addr1, true);
await assertEvent("ApprovalForAll", addr3, addr1, true);
await assertView("isApprovedForAll", [addr3, addr1], true);
await assertFail(ctc0.a.transferFrom(addr2, addr3, 2), "must be nft owner or approved operator");
await ctc2.a.approve(addr0, 2);
await assertView("getApproved", [2], addr0);
await ctc0.a.transferFrom(addr2, addr3, 2);
await assertEvent("Transfer", addr2, addr3, 2);
await assertView("getApproved", [2], zeroAddress);
// addr1 is approved for all for addr3
await ctc1.a.transferFrom(addr3, addr1, 2);
await assertEvent("Transfer", addr3, addr1, 2);
await assertView("isApprovedForAll", [addr3, addr1], true);
await ctc1.a.transferFrom(addr1, addr3, 2);
await assertEvent("Transfer", addr1, addr3, 2);
await ctc3.a.setApprovalForAll(addr1, false);
await assertEvent("ApprovalForAll", addr3, addr1, false);
await assertView("isApprovedForAll", [addr3, addr1], false);
// addr1 is no longer approved
await assertFail(ctc1.a.transferFrom(addr3, addr1, 2));

await assertView("totalSupply", [], 2);
await assertFail(ctc1.a.burn(2), "must be nft owner or approved operator");
await ctc3.a.burn(2);
await assertEvent("Transfer", addr3, zeroAddress, 2);
await assertView("totalSupply", [], 1);

console.log("About to test admin update.");
await assertFail(ctc1.a.updateAdmin(addr2), "must be admin");
await ctc0.a.updateAdmin(addr2);
await assertFail(ctc0.a.updateAdmin(addr3), "must be admin");
await assertFail(ctc0.a.mintTo(addr3), "must be admin");
await ctc2.a.mintTo(addr3)
await assertEvent("Transfer", zeroAddress, addr3, 3);
await assertView("ownerOf", [3], addr3)
await assertView("totalSupply", [], 2);


console.log("Done testing NFT contract.");

214 changes: 214 additions & 0 deletions examples/arc-72/index.rsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
'reach 0.1';
'use strict';

// baseUriLength is the length of ipfs://<v1-CID-in-base32-format>/
const baseUriLength = 67;
const metadataUriType = Bytes(256);
const NftId = UInt256;
// This NFT is limitted to 10^3 tokens for the sake of the `tokenUri` method to construct a URI. But this limit could be raised by extending the URI construction.
const maxNftId = 9999;

export const main = Reach.App(() => {
setOptions({
connectors: [ALGO],
});
const D = Participant('Deployer', {
ready: Fun([], Null),
params: Object({
zeroAddress: Address,
metadataUriBase: Bytes(baseUriLength),
}),
});
const A = API({
transferFrom: Fun([Address, Address, NftId], Null),
approve: Fun([Address, NftId], Null),
setApprovalForAll: Fun([Address, Bool], Null),
updateAdmin: Fun([Address], Null),
mintTo: Fun([Address], NftId),
burn: Fun([NftId], Null),
});
const V = View({
ownerOf: Fun([NftId], Address),
tokenURI: Fun([NftId], metadataUriType),
supportsInterface: Fun([Bytes(4)], Bool),
getApproved: Fun([NftId], Address),
isApprovedForAll: Fun([Address, Address], Bool),
totalSupply: Fun([], UInt256),
currentAdmin: Fun([], Address),

});
const E = Events({
Transfer: [Address, Address, NftId],
Approval: [Address, Address, NftId],
ApprovalForAll: [Address, Address, Bool],
});

init();
D.only(() => {
const params = declassify(interact.params);
});
D.publish(params);

// nftData is [ownerAddress, approvedAddress]
const nftData = new Map(NftId, Tuple(Address, Address));
const getNft = (nftId, mustExist) => {
if (mustExist) {
check(isSome(nftData[nftId]), "nft must exist");
}
return fromSome(nftData[nftId], [params.zeroAddress, params.zeroAddress]);
}
const getNftOwner = (nftId, mustExist) => getNft(nftId, mustExist)[0];
const getNftApproved = (nftId, mustExist) => getNft(nftId, mustExist)[1];

const operatorData = new Map(Tuple(Address, Address), Bool);
const getApprovalForAll = (owner, operator) => {
return fromSome(operatorData[[owner, operator]], false);
}

const canTransfer = (nftId, addr) => {
const [owner, controller] = getNft(nftId, true);
return addr == owner || addr == controller || getApprovalForAll(owner, addr);
}

D.interact.ready();

const vars = parallelReduce({
adminAddress: D,
nMinted: UInt256(0),
totalSupply: UInt256(0),
})
.define(() => {

// helpers
const noPayment = () => [0];
const tokenUri = (nftId) => {
check(isSome(nftData[nftId]), "nft must exist");
const idShort = UInt(nftId, true);
const digitArr = array(Bytes(1),
["0", "1", "2", "3", "4", "5", "6", "7", "8", "9",]);
const digit0 = digitArr[idShort % 10];
const digit1 = digitArr[(idShort / 10) % 10];
const digit2 = digitArr[(idShort / 100) % 10];
const digit3 = digitArr[(idShort / 1000) % 10];
const digits1 = Bytes.concat(digit1, digit0);
const digits2 = Bytes.concat(digit2, digits1);
const digits3 = Bytes.concat(digit3, digits2);
const uri = Bytes.concat(params.metadataUriBase, digits3);
return metadataUriType.pad(uri);
}
const ifaceSelectors = [
// ARC-73 (supportsInterface)
Bytes.fromHex("0x4e22a3ba"),
// ARC-72 Core
Bytes.fromHex("0x15974096"),
// ARC-72 Metadata extension
Bytes.fromHex("0x9112544c"),
// ARC-72 Transfer Management extension
Bytes.fromHex("0x924d64fb"),
];
const supportsInterface = (ifaceSelector) => {
return ifaceSelectors.includes(ifaceSelector);
}

//views
V.ownerOf.set(nftId => getNftOwner(nftId, false));
V.tokenURI.set(tokenUri);
V.supportsInterface.set(supportsInterface);
V.getApproved.set(nftId => getNftApproved(nftId, false));
V.isApprovedForAll.set(getApprovalForAll);
V.totalSupply.set(() => vars.totalSupply);
V.currentAdmin.set(() => vars.adminAddress);
})
.invariant(balance() === 0)
.while(true)
.api_(A.updateAdmin, (newAdmin) => {
check(vars.adminAddress == this, "must be admin")
return [
noPayment(),
(k) => {
k(null);
return {...vars, adminAddress: newAdmin};
},
];
})
.api_(A.mintTo, (firstOwner) => {
check(vars.adminAddress == this, "must be admin");
check(vars.nMinted <= UInt256(maxNftId), "already minted max NFT")
const nftId = vars.nMinted + UInt256(1);
const newTotalSupply = vars.totalSupply + UInt256(1);
return [
noPayment(),
(k) => {
nftData[nftId] = [firstOwner, params.zeroAddress];
E.Transfer(params.zeroAddress, firstOwner, nftId);
k(nftId);
return {
...vars,
nMinted: nftId,
totalSupply: newTotalSupply,
};
},
];
})
.api_(A.approve, (controller, nftId) => {
const owner = getNftOwner(nftId, true);
check(this == owner, "must be nft owner");
return [
noPayment(),
(k) => {
nftData[nftId] = [owner, controller];
E.Approval(owner, controller, nftId);
k(null);
return vars;
},
];
})
.api_(A.setApprovalForAll, (operator, tOrF) => {
return [
noPayment(),
(k) => {
if (tOrF) {
operatorData[[this, operator]] = true;
} else {
delete operatorData[[this, operator]];
}
E.ApprovalForAll(this, operator, tOrF);
k(null);
return vars;
},
];
})
.api_(A.transferFrom, (oldOwner, newOwner, nftId) => {
const oldOwnerReal = getNftOwner(nftId, true);
check(oldOwnerReal == oldOwner, "owner specified in API must be correct");
check(canTransfer(nftId, this), "must be nft owner or approved operator");
return [
noPayment(),
(k) => {
nftData[nftId] = [newOwner, params.zeroAddress];
E.Transfer(oldOwner, newOwner, nftId);
k(null);
return vars;
},
];
})
.api_(A.burn, (nftId) => {
const owner = getNftOwner(nftId, true);
check(canTransfer(nftId, this), "must be nft owner or approved operator");
return [
noPayment(),
(k) => {
delete nftData[nftId];
E.Transfer(owner, params.zeroAddress, nftId);
k(null);
return {
...vars,
totalSupply: vars.totalSupply - UInt256(1),
};
},
];
})
;
commit();
exit();
});
8 changes: 8 additions & 0 deletions examples/arc-72/index.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Compiling `main`...
Verifying knowledge assertions
Verifying for generic connector
Verifying when ALL participants are honest
Verifying when NO participants are honest
Checked 230 theorems; No failures!
WARNING: Compiler instructed to emit for Algorand, but the conservative analysis found these potential problems:
* This program uses 'ALGOExitMode: DeleteAndCloseOutAll_SoundASAs_UnsoundElse' (the default) _and_ creates Map entries and Reach cannot guarantee that these closed at application exit, but we are generating a close out anyways. If those resources are not freed, then this close out will fail and the final transaction will always be rejected.