Skip to content

Commit 4ca635a

Browse files
Non fungible token (#131)
* add new example - NFT * add symlink Co-authored-by: Robert Zaremba <[email protected]>
1 parent 45d2ebd commit 4ca635a

File tree

22 files changed

+337
-35
lines changed

22 files changed

+337
-35
lines changed

examples/asa/.gitignore

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
node_modules/
2-
1+
node_modules/

examples/asa/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require("./algob.config");

examples/asa/package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
2-
"name": "local-project-test",
3-
"version": "0.0.1",
2+
"name": "example-asa",
3+
"version": "1.0.0",
44
"main": "index.js",
5-
"license": "MIT",
5+
"license": "apache-2.0",
66
"devDependencies": {
77
"chai": "^4.2.0"
88
},

examples/asa/scripts/transfer/common.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ const { TransactionType, SignType, executeTransaction } = require("algob");
22

33
exports.executeTransaction = async function (deployer, txnParams) {
44
try {
5-
const details = await executeTransaction(deployer, txnParams);
5+
await executeTransaction(deployer, txnParams);
66
} catch (e) {
77
console.error('Transaction Failed', e.response.error);
88
}

examples/nft/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules/

examples/nft/README.md

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Non-Fungible-Token Example using stateful TEAL
2+
3+
In this example, we create a new non-fungible-token represented by a name and a ref.
4+
Compared to standard ASA, in this example we create a smart-contract to manage and store NFT balances.
5+
6+
- Each NFT is an `(ID, ref_data, hash)` triple, stored in the smart contract as `{id: ref_data, id_h: hash}` mapping.
7+
- `id` is the NFT ID which is calculated as: `total_count_of_nft + 1`. Eg: `id = 1` for the first nft in the smart contract.
8+
- `ref_data` is associated reference data. Ideally the `ref_data` should be an external URL. Eg: `ref_data` = `https://nft.com/1/ref`.
9+
- `id_h` is a `sha256` hash of the content of reference data.
10+
11+
Please check smart contract for the available commands and arguments to the smart contract.
12+
13+
NOTE:
14+
* An account can hold upto a maximum of 8 NFT's in it's local storage.
15+
* The NFT smart contract can hold a maximum of 32 NFT's (in global storage)
16+
* _**This is only a proof of concept**. In production use we need to handle properly the account which can manage the smart-contract (update or delete)_.
17+
* In this ASC, Update Application call is **blocked** i.e once deployed, you cannot update the smart contract by another program. Refer to [this](https://developer.algorand.org/docs/features/asc1/stateful/#update-stateful-smart-contract) for more details.
18+
19+
This is because the ASC can store a maximum of 16 values (`int/[]byte`) in user account local storage and 64 values (`int/[]byte`) in the global storage. Read more about state storage [here](https://developer.algorand.org/docs/features/asc1/stateful/sdks/#state-storage).
20+
21+
## Setup
22+
23+
Please follow the [setup](../README.md) instructions to install dependencies and update the config.
24+
25+
## Run
26+
27+
```
28+
yarn run algob deploy
29+
yarn run algob run scripts/transfer/create-transfer-nft.js
30+
```

examples/nft/algob.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require("config");

examples/nft/assets/nft_approval.py

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
from pyteal import *
2+
3+
def approval_program():
4+
"""
5+
This smart contract implements a Non Fungible Token
6+
Each NFT is an (ID, ref_data, hash) triple.
7+
NFT ID is the total count of NFT's in asc.
8+
ref-data is represented as "https://nft-name/<total>/nft-ref"
9+
hash is an hash of the external resources data.
10+
11+
Commands:
12+
create Creates a new NFT. Expects one additional argument: nft-ref
13+
Only creator can create new NFTs. Ideally, nft-ref should be a URL.
14+
transfer Transfers an NFT between two accounts. Expects one additional arg: NFT_ID.
15+
Additionally, two Accounts(from, to) must also be passed to the smart contract.
16+
"""
17+
18+
var_total = Bytes("total")
19+
20+
# Check to see that the application ID is not set, indicating this is a creation call.
21+
# Store the creator address to global state.
22+
# Set total nft count to 0
23+
on_creation = Seq([
24+
App.globalPut(Bytes("creator"), Txn.sender()),
25+
Assert(Txn.application_args.length() == Int(0)),
26+
App.globalPut(var_total, Int(0)),
27+
Return(Int(1))
28+
])
29+
30+
# Always verify that the RekeyTo property of any transaction is set to the ZeroAddress
31+
# unless the contract is specifically involved ina rekeying operation.
32+
no_rekey_addr = Txn.rekey_to() == Global.zero_address()
33+
34+
# Checks whether the sender is creator.
35+
is_creator = Txn.sender() == App.globalGet(Bytes("creator"))
36+
37+
# Get total amount of NFT's from global storage
38+
nft_id = Itob(App.globalGet(var_total))
39+
40+
# ref_data is a reference data of the NFT, usually an URL or CID.
41+
# nft-hash = hash of ref-data
42+
ref_data = Txn.application_args[1]
43+
ref_hash = Sha256(Txn.application_args[1])
44+
45+
# Verifies if the creater is making this request
46+
# Verifies three arguments are passed to this transaction ("create", nft-name, nft-ref)
47+
# Increment Global NFT count by 1
48+
# Assign ref_data to NFT ID (= total)
49+
# Assign hash of nft-ref to ID_h
50+
# Add above two keys to global and creator's local storage
51+
create_nft = Seq([
52+
Assert(And(
53+
Txn.application_args.length() == Int(2),
54+
is_creator,
55+
no_rekey_addr
56+
)),
57+
58+
App.globalPut(var_total, App.globalGet(var_total) + Int(1)),
59+
60+
App.globalPut(nft_id, ref_data),
61+
App.globalPut(Concat(nft_id, Bytes("_h")) , ref_hash),
62+
63+
App.localPut(Int(0), nft_id, ref_data), # Int(0) represents the address of caller
64+
App.localPut(Int(0), Concat(nft_id, Bytes("_h")) , ref_hash),
65+
Return(is_creator)
66+
])
67+
68+
# nft-hash key from 1st argument
69+
id_h = Concat(Txn.application_args[1], Bytes("_h"))
70+
71+
# Verify two arguments are passed
72+
# Verify NFT_ID is present in global storage
73+
# Add nft to account_2's local storage
74+
# Remove nft from account_1's local storage
75+
transfer_nft = Seq([
76+
Assert(And(
77+
Txn.application_args.length() == Int(2),
78+
App.globalGet(var_total) >= Btoi(Txn.application_args[1]),
79+
no_rekey_addr
80+
)),
81+
82+
App.localPut(Int(2), Txn.application_args[1], App.localGet(Int(1), Txn.application_args[1])),
83+
App.localPut(Int(2), id_h, App.localGet(Int(1), id_h)),
84+
85+
App.localDel(Int(1), Txn.application_args[1]),
86+
App.localDel(Int(1), id_h),
87+
Return(Int(1))
88+
])
89+
90+
# Verfies that the application_id is 0, jumps to on_creation.
91+
# Verifies that DeleteApplication is used and verifies that sender is creator.
92+
# Verifies that UpdateApplication is used and blocks that call (unsafe for production use).
93+
# Verifies that closeOut is used and jumps to on_closeout.
94+
# Verifies that the account has opted in and jumps to on_register.
95+
# Verifies that first argument is "vote" and jumps to on_vote.
96+
program = Cond(
97+
[Txn.application_id() == Int(0), on_creation],
98+
[Txn.on_completion() == OnComplete.UpdateApplication, Return(Int(0))], #block update
99+
[Txn.on_completion() == OnComplete.DeleteApplication, Return(is_creator)],
100+
[Txn.on_completion() == OnComplete.CloseOut, Return(Int(1))],
101+
[Txn.on_completion() == OnComplete.OptIn, Return(Int(1))],
102+
[Txn.application_args[0] == Bytes("create"), create_nft],
103+
[Txn.application_args[0] == Bytes("transfer"), transfer_nft]
104+
)
105+
106+
return program
107+
108+
if __name__ == "__main__":
109+
print(compileTeal(approval_program(), Mode.Application))
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from pyteal import *
2+
3+
def clear_state_program():
4+
return Return(Int(1))
5+
6+
if __name__ == "__main__":
7+
print(compileTeal(clear_state_program(), Mode.Application))

examples/nft/package.json

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "nft-pyteal",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"license": "apache-2.0",
6+
"dependencies": {
7+
"config": "file:../asa",
8+
"algosdk": "^1.8.0"
9+
},
10+
"scripts": {
11+
"algob": "algob"
12+
}
13+
}

examples/nft/scripts/deploy-nft.js

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Description:
3+
* This file deploys the stateful smart contract to create and transfer NFT
4+
*/
5+
const { executeTransaction } = require("./transfer/common");
6+
const { TransactionType, SignType } = require("algob");
7+
8+
async function run(runtimeEnv, deployer) {
9+
10+
const masterAccount = deployer.accountsByName.get("master-account")
11+
const johnAccount = deployer.accountsByName.get("john-account");
12+
13+
let algoTxnParams = {
14+
type: TransactionType.TransferAlgo,
15+
sign: SignType.SecretKey,
16+
fromAccount: masterAccount,
17+
toAccountAddr: johnAccount.addr,
18+
amountMicroAlgos: 401000000,
19+
payFlags: {note: "funding account"}
20+
};
21+
22+
await executeTransaction(deployer, algoTxnParams); //fund john
23+
24+
await deployer.deploySSC("nft_approval.py", "nft_clear_state.py", {
25+
sender: masterAccount,
26+
localInts: 8,
27+
localBytes: 8,
28+
globalInts: 1,
29+
globalBytes: 63
30+
}, {});
31+
32+
const sscInfo = await deployer.getSSC("nft_approval.py", "nft_clear_state.py");
33+
const appId = sscInfo.appID;
34+
console.log(sscInfo);
35+
36+
try {
37+
await deployer.OptInToSSC(masterAccount, appId, {}); //opt-in to asc by master
38+
await deployer.OptInToSSC(johnAccount, appId, {}); //opt-in to asc by john
39+
} catch(e) {
40+
console.log(e);
41+
throw new Error(e);
42+
}
43+
}
44+
45+
module.exports = { default: run }
+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const { executeTransaction } = require("algob");
2+
3+
exports.executeTransaction = async function (deployer, txnParams) {
4+
try {
5+
await executeTransaction(deployer, txnParams);
6+
} catch (e) {
7+
console.error('Transaction Failed', e.response.error);
8+
}
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
const { executeTransaction } = require("./common");
2+
const { TransactionType, SignType, toBytes } = require("algob");
3+
4+
async function run(runtimeEnv, deployer) {
5+
6+
const masterAccount = deployer.accountsByName.get("master-account")
7+
const johnAccount = deployer.accountsByName.get("john-account");
8+
9+
const sscInfo = await deployer.getSSC("nft_approval.py", "nft_clear_state.py");
10+
const appId = sscInfo.appID;
11+
console.log(sscInfo);
12+
13+
const nft_ref = "https://new-nft.com";
14+
15+
//push arguments "create" and nft data
16+
let appArgs = [ toBytes("create"), toBytes(nft_ref) ];
17+
18+
let txnParam = {
19+
type: TransactionType.CallNoOpSSC,
20+
sign: SignType.SecretKey,
21+
fromAccount: masterAccount,
22+
appId: appId,
23+
payFlags: {},
24+
appArgs
25+
};
26+
27+
await executeTransaction(deployer, txnParam); // call to create new nft (with id = 1)
28+
29+
// push arguments "transfer", 1 (NFT ID)
30+
appArgs = [
31+
toBytes("transfer"),
32+
new Uint8Array(8).fill(1, 7), //[0, 0, 0, 0, 0, 0, 0, 1]
33+
];
34+
35+
// account_A = master, account_B = john
36+
const accounts = [masterAccount.addr, johnAccount.addr];
37+
38+
txnParam = {
39+
type: TransactionType.CallNoOpSSC,
40+
sign: SignType.SecretKey,
41+
fromAccount: masterAccount,
42+
appId: appId,
43+
payFlags: {},
44+
appArgs,
45+
accounts
46+
};
47+
48+
//call to transfer nft from master to john
49+
await executeTransaction(deployer, txnParam);
50+
}
51+
52+
module.exports = { default: run }

examples/nft/yarn.lock

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../ref-templates/yarn.lock

examples/permissioned-voting/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"name": "local-project-test",
3-
"version": "0.0.1",
2+
"name": "permissioned-voting-pyteal",
3+
"version": "1.0.0",
44
"main": "index.js",
55
"license": "apache-2.0",
66
"devDependencies": {

examples/permissioned-voting/scripts/vote/vote.js

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { TransactionType, SignType } = require("algob");
1+
const { TransactionType, SignType, toBytes } = require("algob");
22
const { executeTransaction } = require("./common");
33

44
async function run(runtimeEnv, deployer) {
@@ -17,8 +17,7 @@ async function run(runtimeEnv, deployer) {
1717

1818
// App arguments to vote for "candidatea".
1919
appArgs = [
20-
new Uint8Array(Buffer.from('vote')),
21-
new Uint8Array(Buffer.from('candidatea'))
20+
toBytes('vote'), toBytes('candidatea')
2221
];
2322

2423
// Get AppInfo and AssetID from checkpoints.

examples/permissioned-voting/scripts/voting.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { executeTransaction, TransactionType, SignType } = require("algob");
1+
const { executeTransaction, TransactionType, SignType, toBytes } = require("algob");
22

33
/**
44
* Description: Converts Integer into Bytes Array
@@ -73,7 +73,7 @@ async function run(runtimeEnv, deployer) {
7373
console.log(res);
7474

7575
// Register Alice in voting application
76-
reg = [new Uint8Array(Buffer.from('register'))];
76+
reg = [toBytes('register')];
7777

7878
console.log("Opting-In for Alice in voting application");
7979
try {

packages/algob/src/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
} from "./lib/account";
88
import { globalZeroAddress } from "./lib/constants";
99
import { algodCredentialsFromEnv, KMDCredentialsFromEnv } from "./lib/credentials";
10-
import { update } from "./lib/ssc";
10+
import { toBytes, update } from "./lib/ssc";
1111
import { balanceOf, printAssets, printGlobalStateSSC, printLocalStateSSC, readGlobalStateSSC } from "./lib/status";
1212
import { executeTransaction } from "./lib/tx";
1313
import { SignType, TransactionType } from "./types";
@@ -29,5 +29,6 @@ export {
2929
printGlobalStateSSC,
3030
readGlobalStateSSC,
3131
update,
32+
toBytes,
3233
globalZeroAddress
3334
};

packages/algob/src/lib/ssc.ts

+5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import tx from "algosdk";
44
import { AlgobDeployer, TxParams } from "../types";
55
import { mkSuggestedParams } from "./tx";
66

7+
// returns parsed string to Uint8Array
8+
export function toBytes (s: string): Uint8Array {
9+
return new Uint8Array(Buffer.from(s));
10+
}
11+
712
/**
813
* Description: Transaction to update TEAL Programs for a contract.
914
* @param deployer AlgobDeployer

0 commit comments

Comments
 (0)