@leofcoin/standards 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/exports/helpers.js +20 -0
- package/exports/index.d.ts +4 -0
- package/exports/index.js +4 -0
- package/exports/private-voting.js +133 -0
- package/exports/public-voting.js +98 -0
- package/exports/token-receiver.d.ts +19 -0
- package/exports/token-receiver.js +36 -0
- package/exports/token.js +1 -19
- package/exports/voting/private-voting.d.ts +134 -0
- package/exports/voting/public-voting.d.ts +51 -0
- package/exports/voting.d.ts +134 -0
- package/exports/voting.js +133 -0
- package/package.json +21 -2
- package/rollup.config.js +19 -14
- package/src/index.ts +5 -0
- package/src/token-receiver.ts +41 -0
- package/src/voting/private-voting.ts +178 -0
- package/src/voting/public-voting.ts +130 -0
- package/tsconfig.json +5 -3
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// when state is stored it get encoded as a string to so we need to reformat balances back to BigNumbers
|
|
2
|
+
const restoreBalances = (balances) => {
|
|
3
|
+
const _balances = {};
|
|
4
|
+
for (const address in balances) {
|
|
5
|
+
_balances[address] = BigNumber['from'](balances[address]);
|
|
6
|
+
}
|
|
7
|
+
return _balances;
|
|
8
|
+
};
|
|
9
|
+
const restoreApprovals = (approvals) => {
|
|
10
|
+
const _approvals = {};
|
|
11
|
+
for (const owner in approvals) {
|
|
12
|
+
_approvals[owner] = {};
|
|
13
|
+
for (const operator in approvals[owner]) {
|
|
14
|
+
_approvals[owner][operator] = BigNumber['from'](approvals[owner][operator]);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return _approvals;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export { restoreApprovals, restoreBalances };
|
package/exports/index.d.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
export { default as Token } from './token.js';
|
|
2
2
|
export { default as Roles } from './roles.js';
|
|
3
|
+
export { default as TokenReceiver } from './token-receiver.js';
|
|
4
|
+
export { default as PublicVoting } from './voting/public-voting.js';
|
|
5
|
+
export { default as PrivateVoting } from './voting/private-voting.js';
|
|
6
|
+
export * from './helpers.js';
|
package/exports/index.js
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
export { default as Token } from './token.js';
|
|
2
2
|
export { default as Roles } from './roles.js';
|
|
3
|
+
export { default as TokenReceiver } from './token-receiver.js';
|
|
4
|
+
export { default as PublicVoting } from './public-voting.js';
|
|
5
|
+
export { default as PrivateVoting } from './private-voting.js';
|
|
6
|
+
export { restoreApprovals, restoreBalances } from './helpers.js';
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
class PrivateVoting {
|
|
2
|
+
#voters;
|
|
3
|
+
#votes;
|
|
4
|
+
#votingDisabled;
|
|
5
|
+
constructor(state) {
|
|
6
|
+
if (state) {
|
|
7
|
+
this.#voters = state.voters;
|
|
8
|
+
this.#votes = state.votes;
|
|
9
|
+
this.#votingDisabled = state.votingDisabled;
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
this.#voters = [msg.sender];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
get votes() {
|
|
16
|
+
return { ...this.#votes };
|
|
17
|
+
}
|
|
18
|
+
get voters() {
|
|
19
|
+
return { ...this.#voters };
|
|
20
|
+
}
|
|
21
|
+
get votingDisabled() {
|
|
22
|
+
return this.#votingDisabled;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
*
|
|
26
|
+
*/
|
|
27
|
+
get state() {
|
|
28
|
+
return { voters: this.#voters, votes: this.#votes, votingDisabled: this.#votingDisabled };
|
|
29
|
+
}
|
|
30
|
+
get inProgress() {
|
|
31
|
+
return Object.entries(this.#votes)
|
|
32
|
+
.filter(([id, vote]) => !vote.finished)
|
|
33
|
+
.map(([id, vote]) => {
|
|
34
|
+
return { ...vote, id };
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* create vote
|
|
39
|
+
* @param {string} vote
|
|
40
|
+
* @param {string} description
|
|
41
|
+
* @param {number} endTime
|
|
42
|
+
* @param {string} method function to run when agree amount is bigger
|
|
43
|
+
*/
|
|
44
|
+
createVote(title, description, endTime, method, args = []) {
|
|
45
|
+
if (!this.canVote(msg.sender))
|
|
46
|
+
throw new Error(`Not allowed to create a vote`);
|
|
47
|
+
const id = crypto.randomUUID();
|
|
48
|
+
this.#votes[id] = {
|
|
49
|
+
title,
|
|
50
|
+
description,
|
|
51
|
+
method,
|
|
52
|
+
endTime,
|
|
53
|
+
args
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
canVote(address) {
|
|
57
|
+
return this.#voters.includes(address);
|
|
58
|
+
}
|
|
59
|
+
#enoughVotes(id) {
|
|
60
|
+
return this.#voters.length - 2 <= Object.keys(this.#votes[id]).length;
|
|
61
|
+
}
|
|
62
|
+
#endVoting(voteId) {
|
|
63
|
+
let agree = Object.values(this.#votes[voteId].results).filter((result) => result === 1);
|
|
64
|
+
let disagree = Object.values(this.#votes[voteId].results).filter((result) => result === 0);
|
|
65
|
+
this.#votes[voteId].enoughVotes = this.#enoughVotes(voteId);
|
|
66
|
+
if (agree.length > disagree.length && this.#votes[voteId].enoughVotes)
|
|
67
|
+
this[this.#votes[voteId].method](...this.#votes[voteId].args);
|
|
68
|
+
this.#votes[voteId].finished = true;
|
|
69
|
+
}
|
|
70
|
+
vote(voteId, vote) {
|
|
71
|
+
vote = Number(vote);
|
|
72
|
+
if (vote !== 0 && vote !== 0.5 && vote !== 1)
|
|
73
|
+
throw new Error(`invalid vote value ${vote}`);
|
|
74
|
+
if (!this.#votes[voteId])
|
|
75
|
+
throw new Error(`Nothing found for ${voteId}`);
|
|
76
|
+
const ended = new Date().getTime() > this.#votes[voteId].endTime;
|
|
77
|
+
if (ended && !this.#votes[voteId].finished)
|
|
78
|
+
this.#endVoting(voteId);
|
|
79
|
+
if (ended)
|
|
80
|
+
throw new Error('voting already ended');
|
|
81
|
+
if (!this.canVote(msg.sender))
|
|
82
|
+
throw new Error(`Not allowed to vote`);
|
|
83
|
+
this.#votes[voteId][msg.sender] = vote;
|
|
84
|
+
if (this.#enoughVotes(voteId)) {
|
|
85
|
+
this.#endVoting(voteId);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
#disableVoting() {
|
|
89
|
+
this.#votingDisabled = true;
|
|
90
|
+
this.#voters = [];
|
|
91
|
+
}
|
|
92
|
+
#grantVotingPower(address) {
|
|
93
|
+
this.#voters.push(address);
|
|
94
|
+
}
|
|
95
|
+
#revokeVotingPower(address) {
|
|
96
|
+
this.#voters.splice(this.#voters.indexOf(address));
|
|
97
|
+
}
|
|
98
|
+
disableVoting() {
|
|
99
|
+
if (!this.canVote(msg.sender))
|
|
100
|
+
throw new Error('not a allowed');
|
|
101
|
+
if (this.#voters.length === 1)
|
|
102
|
+
this.#disableVoting();
|
|
103
|
+
else {
|
|
104
|
+
this.createVote(`disable voting`, `Warning this disables all voting features forever`, new Date().getTime() + 172800000, '#disableVoting', []);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
grantVotingPower(address, voteId) {
|
|
108
|
+
if (this.#voters.length === 1 && this.canVote(msg.sender))
|
|
109
|
+
this.#grantVotingPower(address);
|
|
110
|
+
else {
|
|
111
|
+
this.createVote(`grant voting power to ${address}`, `Should we grant ${address} voting power?`, new Date().getTime() + 172800000, '#grantVotingPower', [address]);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
revokeVotingPower(address, voteId) {
|
|
115
|
+
if (!this.canVote(msg.sender))
|
|
116
|
+
throw new Error('not a allowed to vote');
|
|
117
|
+
if (this.#voters.length === 1 && address === msg.sender && !this.#votingDisabled)
|
|
118
|
+
throw new Error('only one voter left, disable voting before making this contract voteless');
|
|
119
|
+
if (this.#voters.length === 1)
|
|
120
|
+
this.#revokeVotingPower(address);
|
|
121
|
+
else {
|
|
122
|
+
this.createVote(`revoke voting power for ${address}`, `Should we revoke ${address} it's voting power?`, new Date().getTime() + 172800000, '#revokeVotingPower', [address]);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
sync() {
|
|
126
|
+
for (const vote of this.inProgress) {
|
|
127
|
+
if (vote.endTime < new Date().getTime())
|
|
128
|
+
this.#endVoting(vote.id);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export { PrivateVoting as default };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import TokenReceiver from './token-receiver.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* allows everybody that has a balance greater or equeal then/to tokenAmountToReceive to vote
|
|
5
|
+
*/
|
|
6
|
+
class PublicVoting extends TokenReceiver {
|
|
7
|
+
#votes;
|
|
8
|
+
#votingDisabled;
|
|
9
|
+
constructor(tokenToReceive, tokenAmountToReceive, state) {
|
|
10
|
+
super(tokenToReceive, tokenAmountToReceive, state);
|
|
11
|
+
if (state) {
|
|
12
|
+
this.#votes = state.votes;
|
|
13
|
+
this.#votingDisabled = state.votingDisabled;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
get votes() {
|
|
17
|
+
return { ...this.#votes };
|
|
18
|
+
}
|
|
19
|
+
get votingDisabled() {
|
|
20
|
+
return this.#votingDisabled;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
*
|
|
24
|
+
*/
|
|
25
|
+
get state() {
|
|
26
|
+
return { ...super.state, votes: this.#votes, votingDisabled: this.#votingDisabled };
|
|
27
|
+
}
|
|
28
|
+
get inProgress() {
|
|
29
|
+
return Object.entries(this.#votes)
|
|
30
|
+
.filter(([id, vote]) => !vote.finished)
|
|
31
|
+
.map(([id, vote]) => {
|
|
32
|
+
return { ...vote, id };
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* create vote
|
|
37
|
+
* @param {string} vote
|
|
38
|
+
* @param {string} description
|
|
39
|
+
* @param {number} endTime
|
|
40
|
+
* @param {string} method function to run when agree amount is bigger
|
|
41
|
+
*/
|
|
42
|
+
createVote(title, description, endTime, method, args = []) {
|
|
43
|
+
if (!this.canVote(msg.sender))
|
|
44
|
+
throw new Error(`Not allowed to create a vote`);
|
|
45
|
+
const id = crypto.randomUUID();
|
|
46
|
+
this.#votes[id] = {
|
|
47
|
+
title,
|
|
48
|
+
description,
|
|
49
|
+
method,
|
|
50
|
+
endTime,
|
|
51
|
+
args
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
canVote(address) {
|
|
55
|
+
return this.canPay();
|
|
56
|
+
}
|
|
57
|
+
#endVoting(voteId) {
|
|
58
|
+
let agree = Object.values(this.#votes[voteId].results).filter((result) => result === 1);
|
|
59
|
+
let disagree = Object.values(this.#votes[voteId].results).filter((result) => result === 0);
|
|
60
|
+
if (agree.length > disagree.length && this.#votes[voteId].enoughVotes)
|
|
61
|
+
this[this.#votes[voteId].method](...this.#votes[voteId].args);
|
|
62
|
+
this.#votes[voteId].finished = true;
|
|
63
|
+
}
|
|
64
|
+
async vote(voteId, vote) {
|
|
65
|
+
vote = Number(vote);
|
|
66
|
+
if (vote !== 0 && vote !== 0.5 && vote !== 1)
|
|
67
|
+
throw new Error(`invalid vote value ${vote}`);
|
|
68
|
+
if (!this.#votes[voteId])
|
|
69
|
+
throw new Error(`Nothing found for ${voteId}`);
|
|
70
|
+
const ended = new Date().getTime() > this.#votes[voteId].endTime;
|
|
71
|
+
if (ended && !this.#votes[voteId].finished)
|
|
72
|
+
this.#endVoting(voteId);
|
|
73
|
+
if (ended)
|
|
74
|
+
throw new Error('voting already ended');
|
|
75
|
+
if (!this.canVote(msg.sender))
|
|
76
|
+
throw new Error(`Not allowed to vote`);
|
|
77
|
+
await msg.staticCall(this.tokenToReceive, 'burn', [this.tokenAmountToReceive]);
|
|
78
|
+
this.#votes[voteId][msg.sender] = vote;
|
|
79
|
+
}
|
|
80
|
+
#disableVoting() {
|
|
81
|
+
this.#votingDisabled = true;
|
|
82
|
+
}
|
|
83
|
+
disableVoting() {
|
|
84
|
+
if (!this.canVote(msg.sender))
|
|
85
|
+
throw new Error('not a allowed');
|
|
86
|
+
else {
|
|
87
|
+
this.createVote(`disable voting`, `Warning this disables all voting features forever`, new Date().getTime() + 172800000, '#disableVoting', []);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
sync() {
|
|
91
|
+
for (const vote of this.inProgress) {
|
|
92
|
+
if (vote.endTime < new Date().getTime())
|
|
93
|
+
this.#endVoting(vote.id);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export { PublicVoting as default };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface TokenReceiverState {
|
|
2
|
+
tokenToReceive: address;
|
|
3
|
+
tokenAmountToReceive: typeof BigNumber;
|
|
4
|
+
}
|
|
5
|
+
export default class TokenReceiver {
|
|
6
|
+
#private;
|
|
7
|
+
constructor(tokenToReceive: address, tokenAmountToReceive: typeof BigNumber, state: TokenReceiverState);
|
|
8
|
+
get tokenToReceive(): string;
|
|
9
|
+
get tokenAmountToReceive(): import("@ethersproject/bignumber").BigNumber;
|
|
10
|
+
get state(): {
|
|
11
|
+
tokenToReceive: string;
|
|
12
|
+
tokenAmountToReceive: import("@ethersproject/bignumber").BigNumber;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* check if sender can pay
|
|
16
|
+
* @returns {boolean} promise
|
|
17
|
+
*/
|
|
18
|
+
canPay(): Promise<boolean>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
class TokenReceiver {
|
|
2
|
+
#tokenToReceive;
|
|
3
|
+
#tokenAmountToReceive;
|
|
4
|
+
constructor(tokenToReceive, tokenAmountToReceive, state) {
|
|
5
|
+
if (state) {
|
|
6
|
+
this.#tokenToReceive = state.tokenToReceive;
|
|
7
|
+
this.#tokenAmountToReceive = BigNumber['from'](state.tokenAmountToReceive);
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
this.#tokenToReceive = tokenToReceive;
|
|
11
|
+
this.#tokenAmountToReceive = BigNumber['from'](tokenAmountToReceive);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
get tokenToReceive() {
|
|
15
|
+
return this.#tokenToReceive;
|
|
16
|
+
}
|
|
17
|
+
get tokenAmountToReceive() {
|
|
18
|
+
return this.#tokenAmountToReceive;
|
|
19
|
+
}
|
|
20
|
+
get state() {
|
|
21
|
+
return {
|
|
22
|
+
tokenToReceive: this.#tokenToReceive,
|
|
23
|
+
tokenAmountToReceive: this.#tokenAmountToReceive
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* check if sender can pay
|
|
28
|
+
* @returns {boolean} promise
|
|
29
|
+
*/
|
|
30
|
+
async canPay() {
|
|
31
|
+
const amount = (await msg.staticCall(this.#tokenToReceive, 'balanceOf', [msg.sender]));
|
|
32
|
+
return amount.gte(this.#tokenAmountToReceive);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { TokenReceiver as default };
|
package/exports/token.js
CHANGED
|
@@ -1,24 +1,6 @@
|
|
|
1
|
+
import { restoreBalances, restoreApprovals } from './helpers.js';
|
|
1
2
|
import Roles from './roles.js';
|
|
2
3
|
|
|
3
|
-
// when state is stored it get encoded as a string to so we need to reformat balances back to BigNumbers
|
|
4
|
-
const restoreBalances = (balances) => {
|
|
5
|
-
const _balances = {};
|
|
6
|
-
for (const address in balances) {
|
|
7
|
-
_balances[address] = BigNumber['from'](balances[address]);
|
|
8
|
-
}
|
|
9
|
-
return _balances;
|
|
10
|
-
};
|
|
11
|
-
const restoreApprovals = (approvals) => {
|
|
12
|
-
const _approvals = {};
|
|
13
|
-
for (const owner in approvals) {
|
|
14
|
-
_approvals[owner] = {};
|
|
15
|
-
for (const operator in approvals[owner]) {
|
|
16
|
-
_approvals[owner][operator] = BigNumber['from'](approvals[owner][operator]);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return _approvals;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
4
|
class Token extends Roles {
|
|
23
5
|
/**
|
|
24
6
|
* string
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
export type VoteResult = 0 | 0.5 | 1;
|
|
2
|
+
export type Vote = {
|
|
3
|
+
title: string;
|
|
4
|
+
method: string;
|
|
5
|
+
args: any[];
|
|
6
|
+
description: string;
|
|
7
|
+
endTime: EpochTimeStamp;
|
|
8
|
+
results?: {
|
|
9
|
+
[address: address]: VoteResult;
|
|
10
|
+
};
|
|
11
|
+
finished?: boolean;
|
|
12
|
+
enoughVotes?: boolean;
|
|
13
|
+
};
|
|
14
|
+
export type PrivateVotingState = {
|
|
15
|
+
voters: address[];
|
|
16
|
+
votes: {
|
|
17
|
+
[id: string]: Vote;
|
|
18
|
+
};
|
|
19
|
+
votingDisabled: boolean;
|
|
20
|
+
};
|
|
21
|
+
export interface VoteView extends Vote {
|
|
22
|
+
id: string;
|
|
23
|
+
}
|
|
24
|
+
export default class PrivateVoting {
|
|
25
|
+
#private;
|
|
26
|
+
constructor(state: PrivateVotingState);
|
|
27
|
+
get votes(): {
|
|
28
|
+
[x: string]: Vote;
|
|
29
|
+
};
|
|
30
|
+
get voters(): {
|
|
31
|
+
[x: number]: string;
|
|
32
|
+
length: number;
|
|
33
|
+
toString(): string;
|
|
34
|
+
toLocaleString(): string;
|
|
35
|
+
pop(): string;
|
|
36
|
+
push(...items: string[]): number;
|
|
37
|
+
concat(...items: ConcatArray<string>[]): string[];
|
|
38
|
+
concat(...items: (string | ConcatArray<string>)[]): string[];
|
|
39
|
+
join(separator?: string): string;
|
|
40
|
+
reverse(): string[];
|
|
41
|
+
shift(): string;
|
|
42
|
+
slice(start?: number, end?: number): string[];
|
|
43
|
+
sort(compareFn?: (a: string, b: string) => number): string[];
|
|
44
|
+
splice(start: number, deleteCount?: number): string[];
|
|
45
|
+
splice(start: number, deleteCount: number, ...items: string[]): string[];
|
|
46
|
+
unshift(...items: string[]): number;
|
|
47
|
+
indexOf(searchElement: string, fromIndex?: number): number;
|
|
48
|
+
lastIndexOf(searchElement: string, fromIndex?: number): number;
|
|
49
|
+
every<S extends string>(predicate: (value: string, index: number, array: string[]) => value is S, thisArg?: any): this is S[];
|
|
50
|
+
every(predicate: (value: string, index: number, array: string[]) => unknown, thisArg?: any): boolean;
|
|
51
|
+
some(predicate: (value: string, index: number, array: string[]) => unknown, thisArg?: any): boolean;
|
|
52
|
+
forEach(callbackfn: (value: string, index: number, array: string[]) => void, thisArg?: any): void;
|
|
53
|
+
map<U>(callbackfn: (value: string, index: number, array: string[]) => U, thisArg?: any): U[];
|
|
54
|
+
filter<S_1 extends string>(predicate: (value: string, index: number, array: string[]) => value is S_1, thisArg?: any): S_1[];
|
|
55
|
+
filter(predicate: (value: string, index: number, array: string[]) => unknown, thisArg?: any): string[];
|
|
56
|
+
reduce(callbackfn: (previousValue: string, currentValue: string, currentIndex: number, array: string[]) => string): string;
|
|
57
|
+
reduce(callbackfn: (previousValue: string, currentValue: string, currentIndex: number, array: string[]) => string, initialValue: string): string;
|
|
58
|
+
reduce<U_1>(callbackfn: (previousValue: U_1, currentValue: string, currentIndex: number, array: string[]) => U_1, initialValue: U_1): U_1;
|
|
59
|
+
reduceRight(callbackfn: (previousValue: string, currentValue: string, currentIndex: number, array: string[]) => string): string;
|
|
60
|
+
reduceRight(callbackfn: (previousValue: string, currentValue: string, currentIndex: number, array: string[]) => string, initialValue: string): string;
|
|
61
|
+
reduceRight<U_2>(callbackfn: (previousValue: U_2, currentValue: string, currentIndex: number, array: string[]) => U_2, initialValue: U_2): U_2;
|
|
62
|
+
find<S_2 extends string>(predicate: (value: string, index: number, obj: string[]) => value is S_2, thisArg?: any): S_2;
|
|
63
|
+
find(predicate: (value: string, index: number, obj: string[]) => unknown, thisArg?: any): string;
|
|
64
|
+
findIndex(predicate: (value: string, index: number, obj: string[]) => unknown, thisArg?: any): number;
|
|
65
|
+
fill(value: string, start?: number, end?: number): string[];
|
|
66
|
+
copyWithin(target: number, start: number, end?: number): string[];
|
|
67
|
+
entries(): IterableIterator<[number, string]>;
|
|
68
|
+
keys(): IterableIterator<number>;
|
|
69
|
+
values(): IterableIterator<string>;
|
|
70
|
+
includes(searchElement: string, fromIndex?: number): boolean;
|
|
71
|
+
flatMap<U_3, This = undefined>(callback: (this: This, value: string, index: number, array: string[]) => U_3 | readonly U_3[], thisArg?: This): U_3[];
|
|
72
|
+
flat<A, D extends number = 1>(this: A, depth?: D): FlatArray<A, D>[];
|
|
73
|
+
at(index: number): string;
|
|
74
|
+
[Symbol.iterator](): IterableIterator<string>;
|
|
75
|
+
[Symbol.unscopables]: {
|
|
76
|
+
[x: number]: boolean;
|
|
77
|
+
length?: boolean;
|
|
78
|
+
toString?: boolean;
|
|
79
|
+
toLocaleString?: boolean;
|
|
80
|
+
pop?: boolean;
|
|
81
|
+
push?: boolean;
|
|
82
|
+
concat?: boolean;
|
|
83
|
+
join?: boolean;
|
|
84
|
+
reverse?: boolean;
|
|
85
|
+
shift?: boolean;
|
|
86
|
+
slice?: boolean;
|
|
87
|
+
sort?: boolean;
|
|
88
|
+
splice?: boolean;
|
|
89
|
+
unshift?: boolean;
|
|
90
|
+
indexOf?: boolean;
|
|
91
|
+
lastIndexOf?: boolean;
|
|
92
|
+
every?: boolean;
|
|
93
|
+
some?: boolean;
|
|
94
|
+
forEach?: boolean;
|
|
95
|
+
map?: boolean;
|
|
96
|
+
filter?: boolean;
|
|
97
|
+
reduce?: boolean;
|
|
98
|
+
reduceRight?: boolean;
|
|
99
|
+
find?: boolean;
|
|
100
|
+
findIndex?: boolean;
|
|
101
|
+
fill?: boolean;
|
|
102
|
+
copyWithin?: boolean;
|
|
103
|
+
entries?: boolean;
|
|
104
|
+
keys?: boolean;
|
|
105
|
+
values?: boolean;
|
|
106
|
+
includes?: boolean;
|
|
107
|
+
flatMap?: boolean;
|
|
108
|
+
flat?: boolean;
|
|
109
|
+
at?: boolean;
|
|
110
|
+
[Symbol.iterator]?: boolean;
|
|
111
|
+
readonly [Symbol.unscopables]?: boolean;
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
get votingDisabled(): boolean;
|
|
115
|
+
/**
|
|
116
|
+
*
|
|
117
|
+
*/
|
|
118
|
+
get state(): {};
|
|
119
|
+
get inProgress(): VoteView[];
|
|
120
|
+
/**
|
|
121
|
+
* create vote
|
|
122
|
+
* @param {string} vote
|
|
123
|
+
* @param {string} description
|
|
124
|
+
* @param {number} endTime
|
|
125
|
+
* @param {string} method function to run when agree amount is bigger
|
|
126
|
+
*/
|
|
127
|
+
createVote(title: string, description: string, endTime: EpochTimeStamp, method: string, args?: any[]): void;
|
|
128
|
+
canVote(address: address): boolean;
|
|
129
|
+
vote(voteId: string, vote: VoteResult): void;
|
|
130
|
+
disableVoting(): void;
|
|
131
|
+
grantVotingPower(address: address, voteId: string): void;
|
|
132
|
+
revokeVotingPower(address: address, voteId: string): void;
|
|
133
|
+
sync(): void;
|
|
134
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import TokenReceiver, { TokenReceiverState } from '../token-receiver.js';
|
|
2
|
+
export type VoteResult = 0 | 0.5 | 1;
|
|
3
|
+
export type PublicVote = {
|
|
4
|
+
title: string;
|
|
5
|
+
method: string;
|
|
6
|
+
args: any[];
|
|
7
|
+
description: string;
|
|
8
|
+
endTime: EpochTimeStamp;
|
|
9
|
+
results?: {
|
|
10
|
+
[address: address]: VoteResult;
|
|
11
|
+
};
|
|
12
|
+
finished?: boolean;
|
|
13
|
+
enoughVotes?: boolean;
|
|
14
|
+
};
|
|
15
|
+
export interface PublicVotingState extends TokenReceiverState {
|
|
16
|
+
votes: {
|
|
17
|
+
[id: string]: PublicVote;
|
|
18
|
+
};
|
|
19
|
+
votingDisabled: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface VoteView extends PublicVote {
|
|
22
|
+
id: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* allows everybody that has a balance greater or equeal then/to tokenAmountToReceive to vote
|
|
26
|
+
*/
|
|
27
|
+
export default class PublicVoting extends TokenReceiver {
|
|
28
|
+
#private;
|
|
29
|
+
constructor(tokenToReceive: address, tokenAmountToReceive: typeof BigNumber, state: PublicVotingState);
|
|
30
|
+
get votes(): {
|
|
31
|
+
[x: string]: PublicVote;
|
|
32
|
+
};
|
|
33
|
+
get votingDisabled(): boolean;
|
|
34
|
+
/**
|
|
35
|
+
*
|
|
36
|
+
*/
|
|
37
|
+
get state(): PublicVotingState;
|
|
38
|
+
get inProgress(): VoteView[];
|
|
39
|
+
/**
|
|
40
|
+
* create vote
|
|
41
|
+
* @param {string} vote
|
|
42
|
+
* @param {string} description
|
|
43
|
+
* @param {number} endTime
|
|
44
|
+
* @param {string} method function to run when agree amount is bigger
|
|
45
|
+
*/
|
|
46
|
+
createVote(title: string, description: string, endTime: EpochTimeStamp, method: string, args?: any[]): void;
|
|
47
|
+
canVote(address: address): Promise<boolean>;
|
|
48
|
+
vote(voteId: string, vote: VoteResult): Promise<void>;
|
|
49
|
+
disableVoting(): void;
|
|
50
|
+
sync(): void;
|
|
51
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
export type VoteResult = 0 | 0.5 | 1;
|
|
2
|
+
export type Vote = {
|
|
3
|
+
title: string;
|
|
4
|
+
method: string;
|
|
5
|
+
args: any[];
|
|
6
|
+
description: string;
|
|
7
|
+
endTime: EpochTimeStamp;
|
|
8
|
+
results?: {
|
|
9
|
+
[address: address]: VoteResult;
|
|
10
|
+
};
|
|
11
|
+
finished?: boolean;
|
|
12
|
+
enoughVotes?: boolean;
|
|
13
|
+
};
|
|
14
|
+
export type VotingState = {
|
|
15
|
+
voters: address[];
|
|
16
|
+
votes: {
|
|
17
|
+
[id: string]: Vote;
|
|
18
|
+
};
|
|
19
|
+
votingDisabled: boolean;
|
|
20
|
+
};
|
|
21
|
+
export interface VoteView extends Vote {
|
|
22
|
+
id: string;
|
|
23
|
+
}
|
|
24
|
+
export default class Voting {
|
|
25
|
+
#private;
|
|
26
|
+
constructor(state: VotingState);
|
|
27
|
+
get votes(): {
|
|
28
|
+
[x: string]: Vote;
|
|
29
|
+
};
|
|
30
|
+
get voters(): {
|
|
31
|
+
[x: number]: string;
|
|
32
|
+
length: number;
|
|
33
|
+
toString(): string;
|
|
34
|
+
toLocaleString(): string;
|
|
35
|
+
pop(): string;
|
|
36
|
+
push(...items: string[]): number;
|
|
37
|
+
concat(...items: ConcatArray<string>[]): string[];
|
|
38
|
+
concat(...items: (string | ConcatArray<string>)[]): string[];
|
|
39
|
+
join(separator?: string): string;
|
|
40
|
+
reverse(): string[];
|
|
41
|
+
shift(): string;
|
|
42
|
+
slice(start?: number, end?: number): string[];
|
|
43
|
+
sort(compareFn?: (a: string, b: string) => number): string[];
|
|
44
|
+
splice(start: number, deleteCount?: number): string[];
|
|
45
|
+
splice(start: number, deleteCount: number, ...items: string[]): string[];
|
|
46
|
+
unshift(...items: string[]): number;
|
|
47
|
+
indexOf(searchElement: string, fromIndex?: number): number;
|
|
48
|
+
lastIndexOf(searchElement: string, fromIndex?: number): number;
|
|
49
|
+
every<S extends string>(predicate: (value: string, index: number, array: string[]) => value is S, thisArg?: any): this is S[];
|
|
50
|
+
every(predicate: (value: string, index: number, array: string[]) => unknown, thisArg?: any): boolean;
|
|
51
|
+
some(predicate: (value: string, index: number, array: string[]) => unknown, thisArg?: any): boolean;
|
|
52
|
+
forEach(callbackfn: (value: string, index: number, array: string[]) => void, thisArg?: any): void;
|
|
53
|
+
map<U>(callbackfn: (value: string, index: number, array: string[]) => U, thisArg?: any): U[];
|
|
54
|
+
filter<S_1 extends string>(predicate: (value: string, index: number, array: string[]) => value is S_1, thisArg?: any): S_1[];
|
|
55
|
+
filter(predicate: (value: string, index: number, array: string[]) => unknown, thisArg?: any): string[];
|
|
56
|
+
reduce(callbackfn: (previousValue: string, currentValue: string, currentIndex: number, array: string[]) => string): string;
|
|
57
|
+
reduce(callbackfn: (previousValue: string, currentValue: string, currentIndex: number, array: string[]) => string, initialValue: string): string;
|
|
58
|
+
reduce<U_1>(callbackfn: (previousValue: U_1, currentValue: string, currentIndex: number, array: string[]) => U_1, initialValue: U_1): U_1;
|
|
59
|
+
reduceRight(callbackfn: (previousValue: string, currentValue: string, currentIndex: number, array: string[]) => string): string;
|
|
60
|
+
reduceRight(callbackfn: (previousValue: string, currentValue: string, currentIndex: number, array: string[]) => string, initialValue: string): string;
|
|
61
|
+
reduceRight<U_2>(callbackfn: (previousValue: U_2, currentValue: string, currentIndex: number, array: string[]) => U_2, initialValue: U_2): U_2;
|
|
62
|
+
find<S_2 extends string>(predicate: (value: string, index: number, obj: string[]) => value is S_2, thisArg?: any): S_2;
|
|
63
|
+
find(predicate: (value: string, index: number, obj: string[]) => unknown, thisArg?: any): string;
|
|
64
|
+
findIndex(predicate: (value: string, index: number, obj: string[]) => unknown, thisArg?: any): number;
|
|
65
|
+
fill(value: string, start?: number, end?: number): string[];
|
|
66
|
+
copyWithin(target: number, start: number, end?: number): string[];
|
|
67
|
+
entries(): IterableIterator<[number, string]>;
|
|
68
|
+
keys(): IterableIterator<number>;
|
|
69
|
+
values(): IterableIterator<string>;
|
|
70
|
+
includes(searchElement: string, fromIndex?: number): boolean;
|
|
71
|
+
flatMap<U_3, This = undefined>(callback: (this: This, value: string, index: number, array: string[]) => U_3 | readonly U_3[], thisArg?: This): U_3[];
|
|
72
|
+
flat<A, D extends number = 1>(this: A, depth?: D): FlatArray<A, D>[];
|
|
73
|
+
at(index: number): string;
|
|
74
|
+
[Symbol.iterator](): IterableIterator<string>;
|
|
75
|
+
[Symbol.unscopables]: {
|
|
76
|
+
[x: number]: boolean;
|
|
77
|
+
length?: boolean;
|
|
78
|
+
toString?: boolean;
|
|
79
|
+
toLocaleString?: boolean;
|
|
80
|
+
pop?: boolean;
|
|
81
|
+
push?: boolean;
|
|
82
|
+
concat?: boolean;
|
|
83
|
+
join?: boolean;
|
|
84
|
+
reverse?: boolean;
|
|
85
|
+
shift?: boolean;
|
|
86
|
+
slice?: boolean;
|
|
87
|
+
sort?: boolean;
|
|
88
|
+
splice?: boolean;
|
|
89
|
+
unshift?: boolean;
|
|
90
|
+
indexOf?: boolean;
|
|
91
|
+
lastIndexOf?: boolean;
|
|
92
|
+
every?: boolean;
|
|
93
|
+
some?: boolean;
|
|
94
|
+
forEach?: boolean;
|
|
95
|
+
map?: boolean;
|
|
96
|
+
filter?: boolean;
|
|
97
|
+
reduce?: boolean;
|
|
98
|
+
reduceRight?: boolean;
|
|
99
|
+
find?: boolean;
|
|
100
|
+
findIndex?: boolean;
|
|
101
|
+
fill?: boolean;
|
|
102
|
+
copyWithin?: boolean;
|
|
103
|
+
entries?: boolean;
|
|
104
|
+
keys?: boolean;
|
|
105
|
+
values?: boolean;
|
|
106
|
+
includes?: boolean;
|
|
107
|
+
flatMap?: boolean;
|
|
108
|
+
flat?: boolean;
|
|
109
|
+
at?: boolean;
|
|
110
|
+
[Symbol.iterator]?: boolean;
|
|
111
|
+
readonly [Symbol.unscopables]?: boolean;
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
get votingDisabled(): boolean;
|
|
115
|
+
/**
|
|
116
|
+
*
|
|
117
|
+
*/
|
|
118
|
+
get state(): {};
|
|
119
|
+
get inProgress(): VoteView[];
|
|
120
|
+
/**
|
|
121
|
+
* create vote
|
|
122
|
+
* @param {string} vote
|
|
123
|
+
* @param {string} description
|
|
124
|
+
* @param {number} endTime
|
|
125
|
+
* @param {string} method function to run when agree amount is bigger
|
|
126
|
+
*/
|
|
127
|
+
createVote(title: string, description: string, endTime: EpochTimeStamp, method: string, args?: any[]): void;
|
|
128
|
+
canVote(address: address): boolean;
|
|
129
|
+
vote(voteId: string, vote: VoteResult): void;
|
|
130
|
+
disableVoting(): void;
|
|
131
|
+
grantVotingPower(address: address, voteId: string): void;
|
|
132
|
+
revokeVotingPower(address: address, voteId: string): void;
|
|
133
|
+
sync(): void;
|
|
134
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
class Voting {
|
|
2
|
+
#voters;
|
|
3
|
+
#votes;
|
|
4
|
+
#votingDisabled;
|
|
5
|
+
constructor(state) {
|
|
6
|
+
if (state) {
|
|
7
|
+
this.#voters = state.voters;
|
|
8
|
+
this.#votes = state.votes;
|
|
9
|
+
this.#votingDisabled = state.votingDisabled;
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
this.#voters = [msg.sender];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
get votes() {
|
|
16
|
+
return { ...this.#votes };
|
|
17
|
+
}
|
|
18
|
+
get voters() {
|
|
19
|
+
return { ...this.#voters };
|
|
20
|
+
}
|
|
21
|
+
get votingDisabled() {
|
|
22
|
+
return this.#votingDisabled;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
*
|
|
26
|
+
*/
|
|
27
|
+
get state() {
|
|
28
|
+
return { voters: this.#voters, votes: this.#votes, votingDisabled: this.#votingDisabled };
|
|
29
|
+
}
|
|
30
|
+
get inProgress() {
|
|
31
|
+
return Object.entries(this.#votes)
|
|
32
|
+
.filter(([id, vote]) => !vote.finished)
|
|
33
|
+
.map(([id, vote]) => {
|
|
34
|
+
return { ...vote, id };
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* create vote
|
|
39
|
+
* @param {string} vote
|
|
40
|
+
* @param {string} description
|
|
41
|
+
* @param {number} endTime
|
|
42
|
+
* @param {string} method function to run when agree amount is bigger
|
|
43
|
+
*/
|
|
44
|
+
createVote(title, description, endTime, method, args = []) {
|
|
45
|
+
if (!this.canVote(msg.sender))
|
|
46
|
+
throw new Error(`Not allowed to create a vote`);
|
|
47
|
+
const id = crypto.randomUUID();
|
|
48
|
+
this.#votes[id] = {
|
|
49
|
+
title,
|
|
50
|
+
description,
|
|
51
|
+
method,
|
|
52
|
+
endTime,
|
|
53
|
+
args
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
canVote(address) {
|
|
57
|
+
return this.#voters.includes(address);
|
|
58
|
+
}
|
|
59
|
+
#enoughVotes(id) {
|
|
60
|
+
return this.#voters.length - 2 <= Object.keys(this.#votes[id]).length;
|
|
61
|
+
}
|
|
62
|
+
#endVoting(voteId) {
|
|
63
|
+
let agree = Object.values(this.#votes[voteId].results).filter((result) => result === 1);
|
|
64
|
+
let disagree = Object.values(this.#votes[voteId].results).filter((result) => result === 0);
|
|
65
|
+
this.#votes[voteId].enoughVotes = this.#enoughVotes(voteId);
|
|
66
|
+
if (agree.length > disagree.length && this.#votes[voteId].enoughVotes)
|
|
67
|
+
this[this.#votes[voteId].method](...this.#votes[voteId].args);
|
|
68
|
+
this.#votes[voteId].finished = true;
|
|
69
|
+
}
|
|
70
|
+
vote(voteId, vote) {
|
|
71
|
+
vote = Number(vote);
|
|
72
|
+
if (vote !== 0 && vote !== 0.5 && vote !== 1)
|
|
73
|
+
throw new Error(`invalid vote value ${vote}`);
|
|
74
|
+
if (!this.#votes[voteId])
|
|
75
|
+
throw new Error(`Nothing found for ${voteId}`);
|
|
76
|
+
const ended = new Date().getTime() > this.#votes[voteId].endTime;
|
|
77
|
+
if (ended && !this.#votes[voteId].finished)
|
|
78
|
+
this.#endVoting(voteId);
|
|
79
|
+
if (ended)
|
|
80
|
+
throw new Error('voting already ended');
|
|
81
|
+
if (!this.canVote(msg.sender))
|
|
82
|
+
throw new Error(`Not allowed to vote`);
|
|
83
|
+
this.#votes[voteId][msg.sender] = vote;
|
|
84
|
+
if (this.#enoughVotes(voteId)) {
|
|
85
|
+
this.#endVoting(voteId);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
#disableVoting() {
|
|
89
|
+
this.#votingDisabled = true;
|
|
90
|
+
this.#voters = [];
|
|
91
|
+
}
|
|
92
|
+
#grantVotingPower(address) {
|
|
93
|
+
this.#voters.push(address);
|
|
94
|
+
}
|
|
95
|
+
#revokeVotingPower(address) {
|
|
96
|
+
this.#voters.splice(this.#voters.indexOf(address));
|
|
97
|
+
}
|
|
98
|
+
disableVoting() {
|
|
99
|
+
if (!this.canVote(msg.sender))
|
|
100
|
+
throw new Error('not a allowed');
|
|
101
|
+
if (this.#voters.length === 1)
|
|
102
|
+
this.#disableVoting();
|
|
103
|
+
else {
|
|
104
|
+
this.createVote(`disable voting`, `Warning this disables all voting features forever`, new Date().getTime() + 172800000, '#disableVoting', []);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
grantVotingPower(address, voteId) {
|
|
108
|
+
if (this.#voters.length === 1 && this.canVote(msg.sender))
|
|
109
|
+
this.#grantVotingPower(address);
|
|
110
|
+
else {
|
|
111
|
+
this.createVote(`grant voting power to ${address}`, `Should we grant ${address} voting power?`, new Date().getTime() + 172800000, '#grantVotingPower', [address]);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
revokeVotingPower(address, voteId) {
|
|
115
|
+
if (!this.canVote(msg.sender))
|
|
116
|
+
throw new Error('not a allowed to vote');
|
|
117
|
+
if (this.#voters.length === 1 && address === msg.sender && !this.#votingDisabled)
|
|
118
|
+
throw new Error('only one voter left, disable voting before making this contract voteless');
|
|
119
|
+
if (this.#voters.length === 1)
|
|
120
|
+
this.#revokeVotingPower(address);
|
|
121
|
+
else {
|
|
122
|
+
this.createVote(`revoke voting power for ${address}`, `Should we revoke ${address} it's voting power?`, new Date().getTime() + 172800000, '#revokeVotingPower', [address]);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
sync() {
|
|
126
|
+
for (const vote of this.inProgress) {
|
|
127
|
+
if (vote.endTime < new Date().getTime())
|
|
128
|
+
this.#endVoting(vote.id);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export { Voting as default };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leofcoin/standards",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Contract standards",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -16,8 +16,27 @@
|
|
|
16
16
|
"import": "./exports/roles.js",
|
|
17
17
|
"types": "./exports/roles.d.ts"
|
|
18
18
|
},
|
|
19
|
+
"./public-voting": {
|
|
20
|
+
"import": "./exports/public-voting.js",
|
|
21
|
+
"types": "./exports/public-voting.d.ts"
|
|
22
|
+
},
|
|
23
|
+
"./private-voting": {
|
|
24
|
+
"import": "./exports/private-voting.js",
|
|
25
|
+
"types": "./exports/private-voting.d.ts"
|
|
26
|
+
},
|
|
27
|
+
"./token-receiver": {
|
|
28
|
+
"import": "./exports/token-receiver.js",
|
|
29
|
+
"types": "./exports/token-receiver.d.ts"
|
|
30
|
+
},
|
|
31
|
+
"./helpers": {
|
|
32
|
+
"import": "./exports/helpers.js",
|
|
33
|
+
"types": "./exports/helpers.d.ts"
|
|
34
|
+
},
|
|
19
35
|
"./token.js": "./exports/token.js",
|
|
20
|
-
"./roles.js": "./exports/roles.js"
|
|
36
|
+
"./roles.js": "./exports/roles.js",
|
|
37
|
+
"./public-voting.js": "./exports/public-voting.js",
|
|
38
|
+
"./private-voting.js": "./exports/private-voting.js",
|
|
39
|
+
"./helpers.js": "./exports/helpers.js"
|
|
21
40
|
},
|
|
22
41
|
"scripts": {
|
|
23
42
|
"build": "rollup -c",
|
package/rollup.config.js
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
import typescript from '@rollup/plugin-typescript'
|
|
2
|
-
import tsConfig from './tsconfig.json' assert { type: 'json'}
|
|
2
|
+
import tsConfig from './tsconfig.json' assert { type: 'json' }
|
|
3
3
|
import { execSync } from 'child_process'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
5
|
// const templates = (await readdir('./src/templates')).map(path => join('./src/templates', path))
|
|
9
6
|
const clean = () => {
|
|
10
7
|
execSync('rm -rf www/*.js')
|
|
11
8
|
return
|
|
12
9
|
}
|
|
13
10
|
|
|
14
|
-
export default [
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
11
|
+
export default [
|
|
12
|
+
{
|
|
13
|
+
input: [
|
|
14
|
+
'src/index.ts',
|
|
15
|
+
'src/token.ts',
|
|
16
|
+
'src/roles.ts',
|
|
17
|
+
'src/voting/public-voting.ts',
|
|
18
|
+
'src/voting/private-voting.ts',
|
|
19
|
+
'src/helpers.ts',
|
|
20
|
+
'src/token-receiver.ts'
|
|
21
|
+
],
|
|
22
|
+
output: {
|
|
23
|
+
dir: './exports',
|
|
24
|
+
format: 'es'
|
|
25
|
+
},
|
|
26
|
+
plugins: [typescript(tsConfig)]
|
|
27
|
+
}
|
|
28
|
+
]
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
export { default as Token } from './token.js'
|
|
2
2
|
export { default as Roles } from './roles.js'
|
|
3
|
+
export { default as TokenReceiver } from './token-receiver.js'
|
|
4
|
+
export { default as PublicVoting } from './voting/public-voting.js'
|
|
5
|
+
export { default as PrivateVoting } from './voting/private-voting.js'
|
|
6
|
+
|
|
7
|
+
export * from './helpers.js'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface TokenReceiverState {
|
|
2
|
+
tokenToReceive: address
|
|
3
|
+
tokenAmountToReceive: typeof BigNumber
|
|
4
|
+
}
|
|
5
|
+
export default class TokenReceiver {
|
|
6
|
+
#tokenToReceive: address
|
|
7
|
+
#tokenAmountToReceive: typeof BigNumber
|
|
8
|
+
constructor(tokenToReceive: address, tokenAmountToReceive: typeof BigNumber, state: TokenReceiverState) {
|
|
9
|
+
if (state) {
|
|
10
|
+
this.#tokenToReceive = state.tokenToReceive
|
|
11
|
+
this.#tokenAmountToReceive = BigNumber['from'](state.tokenAmountToReceive)
|
|
12
|
+
} else {
|
|
13
|
+
this.#tokenToReceive = tokenToReceive
|
|
14
|
+
this.#tokenAmountToReceive = BigNumber['from'](tokenAmountToReceive)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get tokenToReceive() {
|
|
19
|
+
return this.#tokenToReceive
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get tokenAmountToReceive() {
|
|
23
|
+
return this.#tokenAmountToReceive
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get state() {
|
|
27
|
+
return {
|
|
28
|
+
tokenToReceive: this.#tokenToReceive,
|
|
29
|
+
tokenAmountToReceive: this.#tokenAmountToReceive
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* check if sender can pay
|
|
35
|
+
* @returns {boolean} promise
|
|
36
|
+
*/
|
|
37
|
+
async canPay(): Promise<boolean> {
|
|
38
|
+
const amount = (await msg.staticCall(this.#tokenToReceive, 'balanceOf', [msg.sender])) as typeof BigNumber
|
|
39
|
+
return amount.gte(this.#tokenAmountToReceive)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
export type VoteResult = 0 | 0.5 | 1
|
|
2
|
+
|
|
3
|
+
export type Vote = {
|
|
4
|
+
title: string
|
|
5
|
+
method: string
|
|
6
|
+
args: any[]
|
|
7
|
+
description: string
|
|
8
|
+
endTime: EpochTimeStamp
|
|
9
|
+
results?: { [address: address]: VoteResult }
|
|
10
|
+
finished?: boolean
|
|
11
|
+
enoughVotes?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type PrivateVotingState = {
|
|
15
|
+
voters: address[]
|
|
16
|
+
votes: {
|
|
17
|
+
[id: string]: Vote
|
|
18
|
+
}
|
|
19
|
+
votingDisabled: boolean
|
|
20
|
+
}
|
|
21
|
+
export interface VoteView extends Vote {
|
|
22
|
+
id: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default class PrivateVoting {
|
|
26
|
+
#voters: PrivateVotingState['voters']
|
|
27
|
+
#votes: PrivateVotingState['votes']
|
|
28
|
+
#votingDisabled: boolean
|
|
29
|
+
|
|
30
|
+
constructor(state: PrivateVotingState) {
|
|
31
|
+
if (state) {
|
|
32
|
+
this.#voters = state.voters
|
|
33
|
+
this.#votes = state.votes
|
|
34
|
+
this.#votingDisabled = state.votingDisabled
|
|
35
|
+
} else {
|
|
36
|
+
this.#voters = [msg.sender]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get votes() {
|
|
41
|
+
return { ...this.#votes }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get voters() {
|
|
45
|
+
return { ...this.#voters }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get votingDisabled() {
|
|
49
|
+
return this.#votingDisabled
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
*
|
|
54
|
+
*/
|
|
55
|
+
get state(): {} {
|
|
56
|
+
return { voters: this.#voters, votes: this.#votes, votingDisabled: this.#votingDisabled }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get inProgress(): VoteView[] {
|
|
60
|
+
return Object.entries(this.#votes)
|
|
61
|
+
.filter(([id, vote]) => !vote.finished)
|
|
62
|
+
.map(([id, vote]) => {
|
|
63
|
+
return { ...vote, id }
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* create vote
|
|
68
|
+
* @param {string} vote
|
|
69
|
+
* @param {string} description
|
|
70
|
+
* @param {number} endTime
|
|
71
|
+
* @param {string} method function to run when agree amount is bigger
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
createVote(title: string, description: string, endTime: EpochTimeStamp, method: string, args: any[] = []) {
|
|
75
|
+
if (!this.canVote(msg.sender)) throw new Error(`Not allowed to create a vote`)
|
|
76
|
+
const id = crypto.randomUUID()
|
|
77
|
+
this.#votes[id] = {
|
|
78
|
+
title,
|
|
79
|
+
description,
|
|
80
|
+
method,
|
|
81
|
+
endTime,
|
|
82
|
+
args
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
canVote(address: address) {
|
|
87
|
+
return this.#voters.includes(address)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#enoughVotes(id) {
|
|
91
|
+
return this.#voters.length - 2 <= Object.keys(this.#votes[id]).length
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#endVoting(voteId) {
|
|
95
|
+
let agree = Object.values(this.#votes[voteId].results).filter((result) => result === 1)
|
|
96
|
+
let disagree = Object.values(this.#votes[voteId].results).filter((result) => result === 0)
|
|
97
|
+
this.#votes[voteId].enoughVotes = this.#enoughVotes(voteId)
|
|
98
|
+
if (agree.length > disagree.length && this.#votes[voteId].enoughVotes)
|
|
99
|
+
this[this.#votes[voteId].method](...this.#votes[voteId].args)
|
|
100
|
+
this.#votes[voteId].finished = true
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
vote(voteId: string, vote: VoteResult) {
|
|
104
|
+
vote = Number(vote) as VoteResult
|
|
105
|
+
if (vote !== 0 && vote !== 0.5 && vote !== 1) throw new Error(`invalid vote value ${vote}`)
|
|
106
|
+
if (!this.#votes[voteId]) throw new Error(`Nothing found for ${voteId}`)
|
|
107
|
+
const ended = new Date().getTime() > this.#votes[voteId].endTime
|
|
108
|
+
if (ended && !this.#votes[voteId].finished) this.#endVoting(voteId)
|
|
109
|
+
if (ended) throw new Error('voting already ended')
|
|
110
|
+
if (!this.canVote(msg.sender)) throw new Error(`Not allowed to vote`)
|
|
111
|
+
this.#votes[voteId][msg.sender] = vote
|
|
112
|
+
if (this.#enoughVotes(voteId)) {
|
|
113
|
+
this.#endVoting(voteId)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#disableVoting() {
|
|
118
|
+
this.#votingDisabled = true
|
|
119
|
+
this.#voters = []
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#grantVotingPower(address) {
|
|
123
|
+
this.#voters.push(address)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#revokeVotingPower(address) {
|
|
127
|
+
this.#voters.splice(this.#voters.indexOf(address))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
disableVoting() {
|
|
131
|
+
if (!this.canVote(msg.sender)) throw new Error('not a allowed')
|
|
132
|
+
if (this.#voters.length === 1) this.#disableVoting()
|
|
133
|
+
else {
|
|
134
|
+
this.createVote(
|
|
135
|
+
`disable voting`,
|
|
136
|
+
`Warning this disables all voting features forever`,
|
|
137
|
+
new Date().getTime() + 172800000,
|
|
138
|
+
'#disableVoting',
|
|
139
|
+
[]
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
grantVotingPower(address: address, voteId: string) {
|
|
145
|
+
if (this.#voters.length === 1 && this.canVote(msg.sender)) this.#grantVotingPower(address)
|
|
146
|
+
else {
|
|
147
|
+
this.createVote(
|
|
148
|
+
`grant voting power to ${address}`,
|
|
149
|
+
`Should we grant ${address} voting power?`,
|
|
150
|
+
new Date().getTime() + 172800000,
|
|
151
|
+
'#grantVotingPower',
|
|
152
|
+
[address]
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
revokeVotingPower(address: address, voteId: string) {
|
|
158
|
+
if (!this.canVote(msg.sender)) throw new Error('not a allowed to vote')
|
|
159
|
+
if (this.#voters.length === 1 && address === msg.sender && !this.#votingDisabled)
|
|
160
|
+
throw new Error('only one voter left, disable voting before making this contract voteless')
|
|
161
|
+
if (this.#voters.length === 1) this.#revokeVotingPower(address)
|
|
162
|
+
else {
|
|
163
|
+
this.createVote(
|
|
164
|
+
`revoke voting power for ${address}`,
|
|
165
|
+
`Should we revoke ${address} it's voting power?`,
|
|
166
|
+
new Date().getTime() + 172800000,
|
|
167
|
+
'#revokeVotingPower',
|
|
168
|
+
[address]
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
sync() {
|
|
174
|
+
for (const vote of this.inProgress) {
|
|
175
|
+
if (vote.endTime < new Date().getTime()) this.#endVoting(vote.id)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import TokenReceiver, { TokenReceiverState } from '../token-receiver.js'
|
|
2
|
+
import PrivateVoting from './private-voting.js'
|
|
3
|
+
|
|
4
|
+
export type VoteResult = 0 | 0.5 | 1
|
|
5
|
+
|
|
6
|
+
export type PublicVote = {
|
|
7
|
+
title: string
|
|
8
|
+
method: string
|
|
9
|
+
args: any[]
|
|
10
|
+
description: string
|
|
11
|
+
endTime: EpochTimeStamp
|
|
12
|
+
results?: { [address: address]: VoteResult }
|
|
13
|
+
finished?: boolean
|
|
14
|
+
enoughVotes?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PublicVotingState extends TokenReceiverState {
|
|
18
|
+
votes: {
|
|
19
|
+
[id: string]: PublicVote
|
|
20
|
+
}
|
|
21
|
+
votingDisabled: boolean
|
|
22
|
+
}
|
|
23
|
+
export interface VoteView extends PublicVote {
|
|
24
|
+
id: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* allows everybody that has a balance greater or equeal then/to tokenAmountToReceive to vote
|
|
29
|
+
*/
|
|
30
|
+
export default class PublicVoting extends TokenReceiver {
|
|
31
|
+
#votes: PublicVotingState['votes']
|
|
32
|
+
#votingDisabled: boolean
|
|
33
|
+
|
|
34
|
+
constructor(tokenToReceive: address, tokenAmountToReceive: typeof BigNumber, state: PublicVotingState) {
|
|
35
|
+
super(tokenToReceive, tokenAmountToReceive, state)
|
|
36
|
+
if (state) {
|
|
37
|
+
this.#votes = state.votes
|
|
38
|
+
this.#votingDisabled = state.votingDisabled
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get votes() {
|
|
43
|
+
return { ...this.#votes }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get votingDisabled() {
|
|
47
|
+
return this.#votingDisabled
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
*
|
|
52
|
+
*/
|
|
53
|
+
get state(): PublicVotingState {
|
|
54
|
+
return { ...super.state, votes: this.#votes, votingDisabled: this.#votingDisabled }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get inProgress(): VoteView[] {
|
|
58
|
+
return Object.entries(this.#votes)
|
|
59
|
+
.filter(([id, vote]) => !vote.finished)
|
|
60
|
+
.map(([id, vote]) => {
|
|
61
|
+
return { ...vote, id }
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* create vote
|
|
66
|
+
* @param {string} vote
|
|
67
|
+
* @param {string} description
|
|
68
|
+
* @param {number} endTime
|
|
69
|
+
* @param {string} method function to run when agree amount is bigger
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
createVote(title: string, description: string, endTime: EpochTimeStamp, method: string, args: any[] = []) {
|
|
73
|
+
if (!this.canVote(msg.sender)) throw new Error(`Not allowed to create a vote`)
|
|
74
|
+
const id = crypto.randomUUID()
|
|
75
|
+
this.#votes[id] = {
|
|
76
|
+
title,
|
|
77
|
+
description,
|
|
78
|
+
method,
|
|
79
|
+
endTime,
|
|
80
|
+
args
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
canVote(address: address) {
|
|
85
|
+
return this.canPay()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#endVoting(voteId) {
|
|
89
|
+
let agree = Object.values(this.#votes[voteId].results).filter((result) => result === 1)
|
|
90
|
+
let disagree = Object.values(this.#votes[voteId].results).filter((result) => result === 0)
|
|
91
|
+
if (agree.length > disagree.length && this.#votes[voteId].enoughVotes)
|
|
92
|
+
this[this.#votes[voteId].method](...this.#votes[voteId].args)
|
|
93
|
+
this.#votes[voteId].finished = true
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async vote(voteId: string, vote: VoteResult) {
|
|
97
|
+
vote = Number(vote) as VoteResult
|
|
98
|
+
if (vote !== 0 && vote !== 0.5 && vote !== 1) throw new Error(`invalid vote value ${vote}`)
|
|
99
|
+
if (!this.#votes[voteId]) throw new Error(`Nothing found for ${voteId}`)
|
|
100
|
+
const ended = new Date().getTime() > this.#votes[voteId].endTime
|
|
101
|
+
if (ended && !this.#votes[voteId].finished) this.#endVoting(voteId)
|
|
102
|
+
if (ended) throw new Error('voting already ended')
|
|
103
|
+
if (!this.canVote(msg.sender)) throw new Error(`Not allowed to vote`)
|
|
104
|
+
await msg.staticCall(this.tokenToReceive, 'burn', [this.tokenAmountToReceive])
|
|
105
|
+
this.#votes[voteId][msg.sender] = vote
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#disableVoting() {
|
|
109
|
+
this.#votingDisabled = true
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
disableVoting() {
|
|
113
|
+
if (!this.canVote(msg.sender)) throw new Error('not a allowed')
|
|
114
|
+
else {
|
|
115
|
+
this.createVote(
|
|
116
|
+
`disable voting`,
|
|
117
|
+
`Warning this disables all voting features forever`,
|
|
118
|
+
new Date().getTime() + 172800000,
|
|
119
|
+
'#disableVoting',
|
|
120
|
+
[]
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
sync() {
|
|
126
|
+
for (const vote of this.inProgress) {
|
|
127
|
+
if (vote.endTime < new Date().getTime()) this.#endVoting(vote.id)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
"module": "NodeNext",
|
|
4
4
|
"target": "es2022",
|
|
5
5
|
"outDir": "./exports",
|
|
6
|
-
"moduleResolution":"NodeNext",
|
|
6
|
+
"moduleResolution": "NodeNext",
|
|
7
7
|
"declaration": true,
|
|
8
8
|
"declarationDir": "./exports"
|
|
9
9
|
},
|
|
10
10
|
"include": [
|
|
11
11
|
"./src/*",
|
|
12
|
-
"./node_modules/@leofcoin/global-types/*"
|
|
12
|
+
"./node_modules/@leofcoin/global-types/*",
|
|
13
|
+
"src/voting/private-voting.ts",
|
|
14
|
+
"src/voting/public-voting.ts"
|
|
13
15
|
]
|
|
14
|
-
}
|
|
16
|
+
}
|