Skip to content

Instantly share code, notes, and snippets.

@zmnv
Created April 23, 2022 12:44
Show Gist options
  • Save zmnv/16157749797616ca888a807646f7cec9 to your computer and use it in GitHub Desktop.
Save zmnv/16157749797616ca888a807646f7cec9 to your computer and use it in GitHub Desktop.
Tezos Blog Smart Contract
// You can use https://smartpy.io/ts-ide to originate or tests.
/**
* Tzblg Object (Child Contract).
* Used as microblog platform to store and moderate any TBytes data.
* Demo: https://tzblg.xyz/c/KT1X7PGL7gRwj4F3zx4S6DXKDEeTAJ6D47FM
*/
export type PermissionsType = {
can_anyone_be_author: TBool;
can_admin_edit_other_authors: TBool;
can_author_remove: TBool;
can_author_update: TBool;
};
interface TzblgObjectTStorage {
posts: TBig_map<TNat,
TRecord<
{
id: TNat;
author: TAddress;
value: TBytes;
metadata: TBytes;
},
['id', ['author', ['value', 'metadata']]]
>
>;
posts_lastId: TNat;
posts_payload_type: TString;
likes: TBig_map<TNat, TMap<TAddress, TMutez>>;
authors: TBig_map<TAddress, TString>,
config: {
admin: TAddress;
paused: TBool;
};
permissions: PermissionsType;
metadata: TBig_map<TString, TBytes>;
sig: TString,
}
enum TzblgObjectErrorCodes {
TZBLG1_REMOVING_DISABLED,
TZBLG1_UPDATING_DISABLED,
TZBLG1_NOT_AUTHORIZED,
TZBLG1_POST_NOT_FOUND,
TZBLG1_AUTHOR_NOT_FOUND,
TZBLG1_POST_ALREADY_EXISTS,
TZBLG1_PAUSED,
TZBLG1_AUTHORS_CANT_LIKE_THEM_SELF,
TZBLG1_CANT_CHANGE_BASE_META,
}
const TZBLG_OBJECT_METADATA = [
['', '0x74657a6f732d73746f726167653a6d657461'],
['meta', '0x7b0d0a20202020226e616d65223a2022547a626c67204f626a656374222c0d0a20202020226465736372697074696f6e223a2022436f6e74656e74204d616e6167656d656e7420536d61727420436f6e7472616374222c0d0a202020202276657273696f6e223a2022545a424c47312d4f424a4543542d4259544553222c0d0a2020202022696e7465726661636573223a205b22545a424c47312d4f424a4543542d323032312d31322d3138225d0d0a7d'],
['name', '0x547a626c67204f626a656374'],
['description', ''],
['imageUri', '']
];
const tzblg_object_initial_storage: TzblgObjectTStorage = {
sig: 'tzbgl1-object',
posts: [],
posts_lastId: 0,
posts_payload_type: 'TString',
likes: [],
config: {
admin: 'tz1hdQscorfqMzFqYxnrApuS5i6QSTuoAp3w',
paused: false,
},
authors: [['tz1hdQscorfqMzFqYxnrApuS5i6QSTuoAp3w', '']],
permissions: {
can_anyone_be_author: true,
can_admin_edit_other_authors: true,
can_author_remove: true,
can_author_update: true,
},
metadata: TZBLG_OBJECT_METADATA
}
class TzblgObjectStorage {
constructor(public storage: TzblgObjectTStorage = tzblg_object_initial_storage) { }
}
class TzblgObjectOffChainViews extends TzblgObjectStorage {
@OffChainView({
pure: true,
name: "get_posts_count",
description: 'Get posts count',
})
get_posts_count = (): TNat => {
return this.storage.posts_lastId;
};
@OffChainView({
pure: true,
name: "get_admin",
description: 'Get admin address',
})
get_admin = (): TAddress => {
return this.storage.config.admin;
};
@OffChainView({
pure: true,
name: "get_metadata",
description: 'Get contract metadata',
})
get_metadata = (): TBig_map<TString, TBytes> => {
return this.storage.metadata;
};
@OffChainView({
pure: true,
name: "get_permissions",
description: 'Get contract permissions',
})
get_permissions = (): PermissionsType => {
return this.storage.permissions;
};
@OffChainView({
pure: true,
name: "is_paused",
description: 'Is contract paused? When paused, only <admin> can call main actions.',
})
is_paused = (): TBool => {
return this.storage.config.paused;
};
@OffChainView({
pure: true,
name: "can_author_remove",
description: 'Is anybody can remove their posts?',
})
can_author_remove = (): TBool => {
return this.storage.permissions.can_author_remove;
};
@OffChainView({
pure: true,
name: "can_author_update",
description: 'Is anybody can update their posts?',
})
can_author_update = (): TBool => {
return this.storage.permissions.can_author_update;
};
@OffChainView({
pure: true,
name: "can_admin_edit_other_authors",
description: 'Can admin edit another authors posts?',
})
can_admin_edit_other_authors = (): TBool => {
return this.storage.permissions.can_admin_edit_other_authors;
};
}
/**
* @description Class that contains the contract metadata builder
*/
class TzblgObjectMetadata extends TzblgObjectOffChainViews {
@MetadataBuilder
metadata = {
"name": "Tzblg Object",
"description": "Content Management Smart Contract",
"version": "TZBLG1-OBJECT-BYTES",
"interfaces": ["TZBLG1-OBJECT-2021-12-18"],
views: [
this.get_posts_count,
this.get_admin,
this.get_metadata,
this.get_permissions,
this.is_paused,
this.can_author_remove,
this.can_author_update,
this.can_admin_edit_other_authors,
],
};
}
class TzblgObjectHelpers extends TzblgObjectMetadata {
/**
* @description Fail if contract is paused (except admin).
*/
@Inline
failIfPaused() {
if (Sp.sender !== this.storage.config.admin) {
Sp.verify(!this.storage.config.paused, TzblgObjectErrorCodes.TZBLG1_PAUSED);
}
}
/**
* @description Fail if the sender is not admin
*/
@Inline
failIfSenderNotAdmin() {
Sp.verify(Sp.sender === this.storage.config.admin, TzblgObjectErrorCodes.TZBLG1_NOT_AUTHORIZED);
}
/**
* @description Fail if the post not found
*/
@Inline
failIfPostNotFound(postId: TNat) {
Sp.verify(this.storage.posts.hasKey(postId), TzblgObjectErrorCodes.TZBLG1_POST_NOT_FOUND);
}
/**
* @description Fail if the post not found
*/
@Inline
failIfAuthorNotFound(author: TAddress) {
Sp.verify(this.storage.authors.hasKey(author), TzblgObjectErrorCodes.TZBLG1_AUTHOR_NOT_FOUND);
}
/**
* @description Fail if the post not found
*/
@Inline
failIfPostAlreadyExists(postId: TNat) {
Sp.verify(!this.storage.posts.hasKey(postId), TzblgObjectErrorCodes.TZBLG1_POST_ALREADY_EXISTS);
}
/**
* @description Fail if the sender is not author of the post
*/
@Inline
failIfSenderNotAuthorOfPost(postId: TNat) {
const author = this.storage.posts.get(postId).author;
Sp.verify(Sp.sender === author, TzblgObjectErrorCodes.TZBLG1_NOT_AUTHORIZED);
}
/**
* @description Fail if the sender is not author of the post
*/
@Inline
failIfSenderNotAuthorOrAdminOfPost(postId: TNat) {
const author = this.storage.posts.get(postId).author;
Sp.verify(Sp.sender === author || Sp.sender === this.storage.config.admin, TzblgObjectErrorCodes.TZBLG1_NOT_AUTHORIZED);
}
/**
* @description Fail if the sender is not author of the post
*/
@Inline
failIfSenderNotAuthorOrAdmin(author: TAddress) {
Sp.verify(Sp.sender === author || Sp.sender === this.storage.config.admin, TzblgObjectErrorCodes.TZBLG1_NOT_AUTHORIZED);
}
/**
* @description Fail if contract was originated with can_author_remove is false.
*/
@Inline
failIfCantRemove() {
Sp.verify(this.storage.permissions.can_author_remove, TzblgObjectErrorCodes.TZBLG1_REMOVING_DISABLED);
}
/**
* @description Fail if contract was originated with can_author_update is false.
*/
@Inline
failIfCantUpdate() {
Sp.verify(this.storage.permissions.can_author_update, TzblgObjectErrorCodes.TZBLG1_UPDATING_DISABLED);
}
/**
* @description Fail if not authors
*/
@Inline
failIfAuthorNotAuthorized() {
Sp.verify(this.storage.authors.hasKey(Sp.sender), TzblgObjectErrorCodes.TZBLG1_NOT_AUTHORIZED);
}
}
@Contract
export class TZBLG1 extends TzblgObjectHelpers {
/**
* @description Add a new post.
* @param {{value: TBytes}} params
*/
@EntryPoint
add_post(params: { value: TBytes; metadata: TBytes }): void {
// COMMENTED TO PROVIDE PUBLISHING FOR ALL
// this.failIfAuthorNotAuthorized();
this.failIfPaused();
const postId: TNat = this.storage.posts_lastId;
this.failIfPostAlreadyExists(postId);
this.storage.posts.set(postId, {
id: postId,
author: Sp.sender,
value: params.value,
metadata: params.metadata,
});
this.storage.posts_lastId = postId + 1;
}
@EntryPoint
remove_post(postId: TNat): void {
this.failIfPaused();
this.failIfCantRemove();
this.failIfPostNotFound(postId);
if (this.storage.permissions.can_admin_edit_other_authors) {
this.failIfSenderNotAuthorOrAdminOfPost(postId);
} else {
this.failIfSenderNotAuthorOfPost(postId);
}
this.storage.posts.remove(postId);
}
@EntryPoint
update_post(params: { postId: TNat; value: TBytes; metadata: TBytes }): void {
this.failIfPaused();
this.failIfCantUpdate();
const postId: TNat = params.postId;
this.failIfPostNotFound(postId);
if (this.storage.permissions.can_admin_edit_other_authors) {
this.failIfSenderNotAuthorOrAdminOfPost(postId);
} else {
this.failIfSenderNotAuthorOfPost(postId);
}
if (params.metadata !== '' as TBytes) {
this.storage.posts.set(postId, {
id: postId,
author: this.storage.posts.get(postId).author,
value: params.value,
metadata: params.metadata,
});
} else {
this.storage.posts.set(postId, {
id: postId,
author: this.storage.posts.get(postId).author,
value: params.value,
metadata: this.storage.posts.get(postId).metadata,
});
}
}
@EntryPoint
set_admin(address: TAddress): void {
this.failIfSenderNotAdmin();
this.storage.config.admin = address;
}
/**
* @description Pause the contract
* @param {TBool} paused
*/
@EntryPoint
pause(paused: TBool): void {
this.failIfSenderNotAdmin();
this.storage.config.paused = paused;
}
@EntryPoint
set_author(author: TAddress, value: TString = ''): void {
this.failIfSenderNotAuthorOrAdmin(author);
if (Sp.sender !== this.storage.config.admin) {
this.failIfPaused();
Sp.verify(this.storage.authors.hasKey(author), TzblgObjectErrorCodes.TZBLG1_NOT_AUTHORIZED);
}
this.storage.authors.set(author, value);
}
@EntryPoint
remove_author(author: TAddress): void {
Sp.verify(Sp.sender === this.storage.config.admin || Sp.sender === author, TzblgObjectErrorCodes.TZBLG1_NOT_AUTHORIZED);
Sp.verify(author !== this.storage.config.admin, TzblgObjectErrorCodes.TZBLG1_NOT_AUTHORIZED);
this.failIfAuthorNotFound(author);
this.storage.authors.remove(author);
}
@EntryPoint
like_post(postId: TNat): void {
this.failIfPostNotFound(postId);
const postAuthor = this.storage.posts.get(postId).author;
Sp.verify(Sp.sender !== postAuthor, TzblgObjectErrorCodes.TZBLG1_AUTHORS_CANT_LIKE_THEM_SELF);
// const authorAlreadyLikedThisPost = this.storage.likes.get(postId).hasKey(Sp.sender);
if (Sp.amount > (0 as TMutez)) {
const contract = Sp.contract<TUnit>(postAuthor).openSome("Invalid Interface");
Sp.transfer(Sp.unit, Sp.amount, contract);
}
const postAlreadyLikedBySomebody = this.storage.likes.hasKey(postId);
if (postAlreadyLikedBySomebody) {
const isSenderAlreadyLikedThisPost = this.storage.likes.get(postId).hasKey(Sp.sender);
if (isSenderAlreadyLikedThisPost) {
const alreadyAmount = this.storage.likes.get(postId).get(Sp.sender);
this.storage.likes.get(postId).set(Sp.sender, Sp.amount + alreadyAmount);
} else {
this.storage.likes.get(postId).set(Sp.sender, Sp.amount);
}
} else {
this.storage.likes.set(postId, [[Sp.sender, Sp.amount]]);
}
}
@EntryPoint
set_metadata(key: TString, value: TBytes) {
this.failIfSenderNotAdmin();
Sp.verify((key !== 'meta' && key !== ''), TzblgObjectErrorCodes.TZBLG1_CANT_CHANGE_BASE_META);
this.storage.metadata.set(key, value)
}
}
/**
* -------------------- [TESTS] Tzblg1 Object -----------------------
*/
Dev.test({ name: 'Object: Posts' }, () => {
Scenario.h2('TZBLG1 Posts');
const admin = Scenario.testAccount('Administrator');
const bob = Scenario.testAccount('Bob');
const guest = Scenario.testAccount('Guest');
Scenario.show([admin, bob, guest]);
Scenario.h4('Originate');
const c1 = Scenario.originate(new TZBLG1(tzblg_object_initial_storage));
Scenario.h4('Add Post by not authorized user');
Scenario.transfer(c1.add_post({ value: '0x48656c6c6f20627920426f6221', metadata: '' }), { sender: bob.address, });
Scenario.h4('Add Bob as author');
Scenario.transfer(c1.set_author(bob.address, 'Bob Dylan Jr.'), { sender: admin.address });
Scenario.h4('Add Post by Bob');
Scenario.transfer(c1.add_post({ value: '0x426f622044796c616e204a722e', metadata: '' }), { sender: bob.address });
Scenario.h4('Update Post by Bob');
Scenario.transfer(c1.update_post({ postId: 0, value: '0x4e65772076616c7565', metadata: '' }), { sender: bob.address });
Scenario.h4('Update Bobs Post by Guest');
Scenario.transfer(c1.update_post({ postId: 0, value: '0x48612d6861206861616161', metadata: '' }), { sender: guest.address, valid: false });
Scenario.h4('Update Bobs Post by Admin');
Scenario.transfer(c1.update_post({ postId: 0, value: '0x4e65772076616c75652e205570646174656421', metadata: '' }), { sender: admin.address });
Scenario.h4('Remove Bob post by Guest');
Scenario.transfer(c1.add_post({ value: '0x4e657720706f737420746f2072656d6f7665', metadata: '' }), { sender: bob.address });
Scenario.transfer(c1.remove_post(0), { sender: guest.address, valid: false });
Scenario.h4('Remove Bob post by Admin');
Scenario.transfer(c1.remove_post(0), { sender: admin.address });
Scenario.h4('Remove Bob post by Bob');
Scenario.transfer(c1.remove_post(1), { sender: bob.address });
});
Dev.test({ name: 'Object: Author' }, () => {
Scenario.h2('TZBLG1 Author');
const admin = Scenario.testAccount('Administrator');
const bob = Scenario.testAccount('Bob');
const guest = Scenario.testAccount('Guest');
Scenario.show([admin, bob, guest]);
Scenario.h4('Originate');
const c1 = Scenario.originate(new TZBLG1(tzblg_object_initial_storage));
Scenario.h4('Add Post by not authorized user');
Scenario.transfer(c1.add_post({ value: '0x48656c6c6f20627920426f6221', metadata: '' }), { sender: bob.address, });
Scenario.h4('Add Bob as author');
Scenario.transfer(c1.set_author(bob.address, 'Bob Dyilan'), { sender: admin.address });
Scenario.h4('Update Bobs name by Bob');
Scenario.transfer(c1.set_author(bob.address, 'Bob Dylan Jr.'), { sender: bob.address });
Scenario.h4('Update Bobs name by Guest');
Scenario.transfer(c1.set_author(bob.address, 'Bob Dylan Jr.'), { sender: guest.address, valid: false });
Scenario.h4('Update Bobs name by Admin');
Scenario.transfer(c1.set_author(bob.address, 'banned'), { sender: admin.address });
Scenario.h4('Remove Bob from Authors by Guest');
Scenario.transfer(c1.remove_author(bob.address), { sender: guest.address, valid: false });
Scenario.h4('Remove Bob from Authors by Bob');
Scenario.transfer(c1.remove_author(bob.address), { sender: bob.address });
Scenario.h4('Remove Admin from Authors by Admin');
Scenario.transfer(c1.remove_author(admin.address), { sender: admin.address, valid: false });
});
Dev.test({ name: 'Object: Admin' }, () => {
Scenario.h2('TZBLG1 Admin');
const admin = Scenario.testAccount('Administrator');
const bob = Scenario.testAccount('Bob');
const guest = Scenario.testAccount('Guest');
Scenario.show([admin, bob, guest]);
Scenario.h4('Originate');
const c1 = Scenario.originate(new TZBLG1(tzblg_object_initial_storage));
Scenario.h4('Set Admin by Bob');
Scenario.transfer(c1.set_admin(bob.address), { sender: bob.address, valid: false });
Scenario.h4('Set Admin by Admin');
Scenario.transfer(c1.set_admin(admin.address), { sender: admin.address });
Scenario.h4('Set Admin by Guest');
Scenario.transfer(c1.set_admin(admin.address), { sender: guest.address, valid: false });
Scenario.h4('Set Bob as admin and pause contract');
Scenario.transfer(c1.set_admin(bob.address), { sender: admin.address });
Scenario.p('by admin:');
Scenario.transfer(c1.pause(true), { sender: admin.address, valid: false });
Scenario.p('by bob (new admin):');
Scenario.transfer(c1.pause(true), { sender: bob.address });
});
Dev.test({ name: 'Object: Likes' }, () => {
Scenario.h2('TZBLG1 Likes');
const admin = Scenario.testAccount('Administrator');
const bob = Scenario.testAccount('Bob');
const guest = Scenario.testAccount('Guest');
Scenario.show([admin, bob, guest]);
Scenario.h4('Originate');
const c1 = Scenario.originate(new TZBLG1(tzblg_object_initial_storage));
Scenario.h4('Add Bob as Author');
Scenario.transfer(c1.set_author(bob.address, 'Bob Dylan Jr.'), { sender: admin.address });
Scenario.h4('Add post by Bob');
Scenario.transfer(c1.add_post({ value: '0x68656c6c6f', metadata: '' }), { sender: bob.address });
Scenario.h4('Like post by Bob (author)');
Scenario.transfer(c1.like_post(0), { sender: bob.address, valid: false });
Scenario.h4('Like post by Guest');
Scenario.transfer(c1.like_post(0), { sender: guest.address });
Scenario.h4('Like post by Guest (again)');
Scenario.transfer(c1.like_post(0), { sender: guest.address });
Scenario.h4('Like post by Guest (again with money)');
Scenario.transfer(c1.like_post(0), { sender: guest.address, amount: 1000000 });
Scenario.h4('Like post by Guest (again with money and again)');
Scenario.transfer(c1.like_post(0), { sender: guest.address, amount: 1000000 });
Scenario.h4('Like post by Admin');
Scenario.transfer(c1.like_post(0), { sender: admin.address });
});
Dev.test({ name: 'Object: Pause' }, () => {
Scenario.h2('TZBLG1 Pause');
const admin = Scenario.testAccount('Administrator');
const bob = Scenario.testAccount('Bob');
const guest = Scenario.testAccount('Guest');
Scenario.show([admin, bob, guest]);
Scenario.h4('Originate');
const c1 = Scenario.originate(new TZBLG1(tzblg_object_initial_storage));
Scenario.h4('Set Pause as Guest');
Scenario.transfer(c1.pause(true), { sender: guest.address, valid: false });
Scenario.h4('Set Pause as Admin');
Scenario.transfer(c1.pause(true), { sender: admin.address });
Scenario.h4('Do something when pause');
Scenario.transfer(c1.add_post({ value: '0x68656c6c6f20776f726c64206c6f6c', metadata: '' }), { sender: guest.address, valid: false });
Scenario.transfer(c1.add_post({ value: '0x62792061646d696e', metadata: '' }), { sender: admin.address, });
Scenario.transfer(c1.set_author(bob.address, 'Hel1o Kitty'), { sender: admin.address });
Scenario.transfer(c1.add_post({ value: '0x68656c6c6f20776f726c64206c6f6c', metadata: '' }), { sender: bob.address, valid: false });
});
Dev.test({ name: 'Object: Set Metadata' }, () => {
Scenario.h2('TZBLG1 Set Metadata');
const admin = Scenario.testAccount('Administrator');
const bob = Scenario.testAccount('Bob');
const guest = Scenario.testAccount('Guest');
Scenario.show([admin, bob, guest]);
Scenario.h4('Originate');
const c1 = Scenario.originate(new TZBLG1(tzblg_object_initial_storage));
Scenario.h4('Set Metadata as Guest');
Scenario.transfer(c1.set_metadata('imageUri', '0xFFFFFF'), { sender: guest.address, valid: false });
Scenario.h4('Set Metadata as Admin');
Scenario.transfer(c1.set_metadata('imageUri', '0x697066733a2f2f516d5147435a71484e3762545572724b387772384b357239656f41504c356864564732563661727a673147627266'), { sender: admin.address });
Scenario.h4('Set Image again as Admin');
Scenario.transfer(c1.set_metadata('imageUri', '0x697066733a2f2f516d66353675586851716f70376d6f6872456463764d77565947476250326b346259455765473938347061745052'), { sender: admin.address });
Scenario.h4('Try to change base meta');
Scenario.transfer(c1.set_metadata('meta', '0x00'), { sender: admin.address, valid: false });
Scenario.h4('Try to change base meta');
Scenario.transfer(c1.set_metadata('', '0x00'), { sender: admin.address, valid: false });
});
const config: TzblgObjectTStorage = {
sig: 'tzbgl1-object',
posts: [],
posts_lastId: 0,
posts_payload_type: 'TBytes',
likes: [],
config: {
admin: 'tz1PNE1qSjGFYMasZFhktcboZA4ryBZH5MAg',
paused: false,
},
authors: [['tz1PNE1qSjGFYMasZFhktcboZA4ryBZH5MAg', 'Creator']],
permissions: {
can_anyone_be_author: true,
can_admin_edit_other_authors: true,
can_author_remove: true,
can_author_update: true,
},
metadata: TZBLG_OBJECT_METADATA
}
Dev.compileContract('compile_contract', new TZBLG1(config));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment