├── LICENSE
├── README.md
├── assets
├── javascripts
│ └── discourse
│ │ ├── components
│ │ ├── eth-user.js.es6
│ │ └── send-eth-button.js.es6
│ │ ├── connectors
│ │ └── user-preferences-account
│ │ │ └── discourse-ethereum.js.es6
│ │ ├── controllers
│ │ └── send-eth.js.es6
│ │ ├── helpers
│ │ └── ethereum.js.es6
│ │ ├── initializers
│ │ └── extend-for-discourse-ethereum.js.es6
│ │ └── templates
│ │ ├── components
│ │ ├── eth-user.hbs
│ │ └── send-eth-button.hbs
│ │ ├── connectors
│ │ ├── user-card-additional-controls
│ │ │ └── discourse-ethereum.hbs
│ │ ├── user-preferences-account
│ │ │ └── discourse-ethereum.hbs
│ │ └── user-profile-controls
│ │ │ └── discourse-ethereum.hbs
│ │ └── modal
│ │ └── send-eth.hbs
└── stylesheets
│ ├── common.scss
│ └── mobile.scss
├── config
├── locales
│ ├── client.en.yml
│ └── server.en.yml
└── settings.yml
├── jobs
└── send_tx_details.rb
├── lib
└── ethereum.rb
├── plugin.rb
└── screenshot
├── pm-tx-details-erc20.png
├── pm-tx-details-eth.png
├── send-eth-modal-1.png
├── send-eth-modal-2.png
├── send-eth-modal-3.png
├── send-eth-user-card.png
└── send-eth-user-profile.png
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Santiment LLC
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # discourse-ethereum
2 |
3 | ## Requirements
4 |
5 | [MetaMask](https://metamask.io/) installed on your browser.
6 |
7 | ## Screenshot
8 | ### 'Send ETH' Button
9 |
10 | 
11 | 
12 |
13 | ### 'Send ETH' Modal
14 |
15 | 
16 | 
17 | 
18 |
19 | ### PM
20 | PM containing the transaction details sent by `system` to both user:
21 |
22 | #### Transfer ETH
23 | 
24 |
25 | #### Transfer ERC20 Token
26 | 
27 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/components/eth-user.js.es6:
--------------------------------------------------------------------------------
1 | import computed from "ember-addons/ember-computed-decorators";
2 | import { etherscanURL } from "../controllers/send-eth";
3 |
4 | export default Ember.Component.extend({
5 |
6 | classNames: ["eth-user"],
7 |
8 | @computed("site.mobileView")
9 | avatarSize(mobileView) {
10 | return (mobileView ? "large" : "extra_large");
11 | },
12 |
13 | @computed("ethereumAddress")
14 | etherscanURL(ethAddress) {
15 | return etherscanURL("address", ethAddress);
16 | },
17 |
18 | @computed("ethereumAddress")
19 | formatedEthereumAddress(ethAddress) {
20 | return ethAddress.slice(0, 10) + "..." + ethAddress.slice(37);
21 | }
22 |
23 | });
24 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/components/send-eth-button.js.es6:
--------------------------------------------------------------------------------
1 | import showModal from "discourse/lib/show-modal";
2 | import computed from "ember-addons/ember-computed-decorators";
3 |
4 | export default Ember.Component.extend({
5 | tagName: "span",
6 |
7 | @computed("model.can_do_eth_transaction")
8 | disabled(canDoTransaction) {
9 | return ( !canDoTransaction || (typeof window.web3 == "undefined") || !window.web3.eth.defaultAccount );
10 | },
11 |
12 | actions: {
13 | showSendEthModal() {
14 | if (this.get("disabled")) return;
15 | showModal("send-eth", { model: this.get("model") });
16 |
17 | if (this.get("close")) this.sendAction("close");
18 | }
19 | }
20 | });
21 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/connectors/user-preferences-account/discourse-ethereum.js.es6:
--------------------------------------------------------------------------------
1 | export default {
2 |
3 | shouldRender({ model }, component) {
4 | return model.get("eth_enabled_for_user") && component.siteSettings.discourse_ethereum_enabled;
5 | },
6 |
7 | setupComponent({ model }, _component) {
8 | model.set("custom_fields.ethereum_address", model.get("ethereum_address"));
9 | }
10 |
11 | };
12 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/controllers/send-eth.js.es6:
--------------------------------------------------------------------------------
1 | import ModalFunctionality from "discourse/mixins/modal-functionality";
2 | import { default as computed, observes } from "ember-addons/ember-computed-decorators";
3 | import getUrl from "discourse-common/lib/get-url";
4 | import { ajax } from "discourse/lib/ajax";
5 |
6 | function networkPrefix() {
7 | const networkID = web3.version.network;
8 | let prefix;
9 |
10 | switch (networkID) {
11 | case "1":
12 | prefix = "";
13 | break;
14 | case "3":
15 | prefix = "ropsten.";
16 | break;
17 | case "4":
18 | prefix = "rinkeby.";
19 | break;
20 | case "42":
21 | prefix = "kovan.";
22 | break;
23 | }
24 |
25 | return prefix;
26 | }
27 |
28 | export function etherscanURL(path, address) {
29 | const prefix = networkPrefix();
30 |
31 | if (prefix) {
32 | return `https://${prefix}etherscan.io/${path}/${address}`;
33 | }
34 | }
35 |
36 | function fromWei(bigNumber) {
37 | return web3.fromWei(bigNumber.toNumber());
38 | }
39 |
40 | const ABI = [
41 | {"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},
42 | {"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"type":"function"},
43 | {"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},
44 | {"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"showMeTheMoney","outputs":[],"payable":false,"type":"function"} // for development
45 | ];
46 |
47 | export default Ember.Controller.extend(ModalFunctionality, {
48 |
49 | erc20enabled: Ember.computed.notEmpty("siteSettings.discourse_ethereum_erc20_token"),
50 |
51 | onShow() {
52 | this.setProperties({
53 | isLoading: false,
54 | amount: 0,
55 | isSuccess: false,
56 | transactionID: null,
57 | senderAddress: web3.eth.defaultAccount,
58 | symbol: "ETH"
59 | });
60 |
61 | const symbols = ["ETH"];
62 |
63 | this.set("symbols", symbols);
64 |
65 | if (this.get("erc20enabled")) {
66 | this.set("contract", web3.eth.contract(ABI).at(this.siteSettings.discourse_ethereum_erc20_token));
67 |
68 | this.get("contract").symbol((e, symbol) => {
69 | if (!e) this.set("symbols", ["ETH", symbol])
70 | });
71 | }
72 |
73 | this.notifyPropertyChange("symbol");
74 | },
75 |
76 | // observers
77 | @observes("symbol")
78 | setup() {
79 | this.set("_balance", null);
80 | this[`setup${ this.get("symbol") == "ETH" ? "ETH" : "ERC20" }`]();
81 | },
82 |
83 | // computed properties
84 | @computed("_balance")
85 | balance(balance) {
86 | if (!balance) return;
87 |
88 | return parseFloat(fromWei(balance));
89 | },
90 |
91 | @computed("balance")
92 | formatedBalance(balance) {
93 | if (!balance) return;
94 |
95 | return balance.toFixed(5);
96 | },
97 |
98 | @computed("isLoading", "balance", "formatedAmount")
99 | isDisabled(isLoading, balance, amount) {
100 | return (isLoading || !balance || isNaN(amount) || parseFloat(amount) < 0 || parseFloat(amount) > balance);
101 | },
102 |
103 | @computed("amount")
104 | formatedAmount(amount) {
105 | return parseFloat(amount);
106 | },
107 |
108 | // instance functions
109 | updateModal(opts) {
110 | opts = opts || {}
111 |
112 | opts.title = "discourse_ethereum.send_ethereum"
113 |
114 | this.appEvents.trigger("modal:body-shown", opts);
115 | },
116 |
117 | process() {
118 | this.set("isLoading", true);
119 |
120 | this.updateModal({ dismissable: false });
121 |
122 | window.withWeb3().then( ()=>{
123 | const to = this.get("model.ethereum_address");
124 | const value = web3.toWei(this.get("formatedAmount"));
125 | const method = `process${ this.get("symbol") == "ETH" ? "ETH" : "ERC20" }`;
126 |
127 | return this[method](to, value);
128 | });
129 | },
130 |
131 | setupETH() {
132 | web3.eth.getBalance(this.get("senderAddress"), (e, balance) => this.setBalance(e, balance) );
133 | },
134 |
135 | setupERC20() {
136 | this.get("contract").balanceOf(this.get("senderAddress"), (e, balance) => this.setBalance(e, balance) );
137 | },
138 |
139 | setBalance(e, balance) {
140 | e ? console.error(e) : this.set("_balance", balance);
141 | },
142 |
143 | processETH(to, value) {
144 | const args = { from: this.get("senderAddress"), to, value };
145 |
146 | web3.eth.sendTransaction(args, (e, txID) => this.afterProcess(e, txID) );
147 | },
148 |
149 | processERC20(to, value) {
150 | this.get("contract").transfer(to, value, (e, txID) => this.afterProcess(e, txID) );
151 | },
152 |
153 | afterProcess(e, txID) {
154 | e ? this.error(e) : this.success(txID);
155 | },
156 |
157 | success(transactionID) {
158 | web3.eth.getTransaction(transactionID, (err, tx) => {
159 | if (err) return this.error(err);
160 |
161 | const txData = {
162 | hash: transactionID,
163 | from: {
164 | username: this.currentUser.get("username"),
165 | address: this.get("senderAddress")
166 | },
167 | to: {
168 | username: this.get("model.username"),
169 | address: this.get("model.ethereum_address")
170 | },
171 | symbol: this.get("symbol"),
172 | net_prefix: networkPrefix()
173 | };
174 |
175 | if (this.get("symbol") == "ETH") {
176 | txData.value = this.get("formatedAmount");
177 | } else {
178 | txData.token_transfered = this.get("formatedAmount");
179 | txData.token = this.siteSettings.discourse_ethereum_erc20_token
180 | }
181 |
182 | if (tx) {
183 | txData.gas = tx.gas;
184 | txData.gas_price = fromWei(tx.gasPrice);
185 | }
186 |
187 | // create topic
188 | ajax(getUrl("/ethereum"), {
189 | type: "POST",
190 | data: {
191 | tx: txData
192 | }
193 | }).then((result) => {
194 | // bg job is created
195 | this.setProperties({
196 | isLoading: false,
197 | isSuccess: true,
198 | transactionID
199 | });
200 |
201 | this.updateModal();
202 | }).catch(this.error);
203 | });
204 |
205 | },
206 |
207 | error(error) {
208 | console.error(error);
209 |
210 | this.flash(I18n.t("discourse_ethereum.error_message"), "alert-error");
211 | this.set("isLoading", false);
212 | this.updateModal();
213 | },
214 |
215 | actions: {
216 | send() {
217 | if (this.get("isDisabled")) return;
218 |
219 | this.clearFlash();
220 | this.process();
221 | }
222 | }
223 |
224 | });
225 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/helpers/ethereum.js.es6:
--------------------------------------------------------------------------------
1 | import { registerHelper } from 'discourse-common/lib/helpers';
2 |
3 | export default registerHelper("eq", function(params) {
4 | return params[0] == params[1];
5 | });
6 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/initializers/extend-for-discourse-ethereum.js.es6:
--------------------------------------------------------------------------------
1 | import { withPluginApi } from "discourse/lib/plugin-api";
2 | import PreferencesAccount from "discourse/controllers/preferences/account";
3 |
4 | function initWithApi(api) {
5 |
6 | PreferencesAccount.reopen({
7 |
8 | saveAttrNames: ["name", "title", "custom_fields"],
9 |
10 | setEthAddressFor(obj) {
11 | obj.set("ethereum_address", this.get("model.custom_fields.ethereum_address"));
12 | },
13 |
14 | _updateEthereumAddress: function() {
15 | if (!this.siteSettings.discourse_ethereum_enabled) return;
16 |
17 | if (this.get("saved")) {
18 | this.setEthAddressFor(this.get("model"));
19 | }
20 | }.observes("saved")
21 |
22 | });
23 |
24 | window.withWeb3 = function () {
25 | if(window.web3) {
26 | return Promise.resolve(window.web3);
27 | } else if(window.ethereum) {
28 | return window.ethereum.enable()
29 | .then(()=> {
30 | window.web3 = new Web3(ethereum);
31 | return window.web3;
32 | })
33 | .catch( (error)=>{
34 | console.log("User denied account access...", error);
35 | throw error;
36 | })
37 | } else {
38 | console.log("Non-Ethereum browser detected. You should consider trying Metamask!");
39 | return Promise.reject("No web3 detected");
40 | }
41 | }
42 | }
43 |
44 | export default {
45 | name: "extend-for-discourse-ethereum",
46 | initialize() { withPluginApi("0.1", initWithApi); }
47 | };
48 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/templates/components/eth-user.hbs:
--------------------------------------------------------------------------------
1 | {{#link-to "user.summary" user.username}}
2 | {{bound-avatar-template user.avatar_template avatarSize}}
3 | {{/link-to}}
4 |
5 |
{{user.username}}
6 | {{formatedEthereumAddress}}
7 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/templates/components/send-eth-button.hbs:
--------------------------------------------------------------------------------
1 | {{d-button
2 | class="btn-primary"
3 | label="discourse_ethereum.send_eth_button"
4 | action="showSendEthModal"
5 | disabled=disabled
6 | }}
7 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/templates/connectors/user-card-additional-controls/discourse-ethereum.hbs:
--------------------------------------------------------------------------------
1 | {{#if siteSettings.discourse_ethereum_enabled}}
2 |
3 | -
4 | {{send-eth-button model=user close=close}}
5 |
6 |
7 | {{/if}}
8 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/templates/connectors/user-preferences-account/discourse-ethereum.hbs:
--------------------------------------------------------------------------------
1 | {{#if siteSettings.discourse_ethereum_enabled}}
2 |
3 |
4 |
5 |
6 |
7 | {{input type="text" value=model.custom_fields.ethereum_address}}
8 |
9 |
10 |
11 | {{/if}}
12 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/templates/connectors/user-profile-controls/discourse-ethereum.hbs:
--------------------------------------------------------------------------------
1 | {{#if siteSettings.discourse_ethereum_enabled}}
2 | {{send-eth-button model=model}}
3 | {{/if}}
4 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/templates/modal/send-eth.hbs:
--------------------------------------------------------------------------------
1 | {{#d-modal-body id="send-eth" title="discourse_ethereum.send_ethereum"}}
2 |
3 | {{#conditional-loading-spinner condition=isLoading}}
4 | {{#if isSuccess}}
5 |
6 | {{d-icon "check"}}
7 |
8 |
{{i18n "discourse_ethereum.transaction_id"}}
9 | {{ transactionID }}
10 |
11 |
12 | {{i18n "discourse_ethereum.success_message"}}
13 |
14 |
15 | {{d-button
16 | label="discourse_ethereum.ok"
17 | action="closeModal"
18 | class="btn-large btn-primary eth-ok-btn"
19 | }}
20 |
21 | {{else}}
22 |
23 | {{eth-user user=currentUser ethereumAddress=senderAddress}}
24 |
25 |
26 | {{d-icon "arrow-right"}}
27 |
28 |
29 | {{eth-user user=model ethereumAddress=model.ethereum_address}}
30 |
31 |
32 |
68 | {{/if}}
69 | {{/conditional-loading-spinner}}
70 |
71 | {{#if isLoading}}
72 |
73 |
74 | {{i18n "discourse_ethereum.processing"}}
75 |
76 |
77 | {{/if}}
78 |
79 | {{/d-modal-body}}
80 |
--------------------------------------------------------------------------------
/assets/stylesheets/common.scss:
--------------------------------------------------------------------------------
1 | .user-card-additional-controls-outlet.discourse-ethereum {
2 | clear: right;
3 | float: right;
4 | }
5 |
6 | .user-main .about:not(.collapsed-info) .user-profile-controls-outlet.discourse-ethereum button {
7 | width: 100%;
8 | margin-bottom: 10px;
9 | }
10 |
11 | #send-eth {
12 | text-align: center;
13 | }
14 |
15 | .eth-transaction-info {
16 | width: 100%;
17 | display: flex;
18 | margin: 1rem 0 3rem;
19 | }
20 |
21 | .eth-user {
22 | width: 200px;
23 | padding: 1rem;
24 | background-color: $primary-very-low;
25 | }
26 |
27 | .eth-user-gap {
28 | flex: 1;
29 | font-size: 3rem;
30 | color: $tertiary;
31 | align-self: center;
32 | margin: 20px;
33 | }
34 |
35 | .eth-transaction-info, .eth-input {
36 | text-align: center;
37 | }
38 |
39 | .eth-username {
40 | margin-top: 10px;
41 | font-size: 1.15rem;
42 | font-weight: bold;
43 | display: block;
44 | }
45 |
46 | .eth-address {
47 | color: $primary-medium;
48 | font-size: 0.85rem;
49 | word-wrap: break-word;
50 |
51 | &:visited, &:hover {
52 | color: $primary-medium;
53 | }
54 | }
55 |
56 | .eth-input {
57 | margin: 20px 0 10px;
58 | }
59 |
60 | .eth-inputs {
61 | display: inline-flex;
62 | margin-bottom: 20px;
63 |
64 | select {
65 | background-color: $primary-very-low;
66 | width: 75px;
67 | border-left: 0;
68 | }
69 |
70 | input, select {
71 | border-color: $primary-low-mid;
72 | }
73 |
74 | }
75 |
76 | .eth-balance {
77 | margin-bottom: 10px;
78 |
79 | > span {
80 | color: $primary-high;
81 | }
82 | }
83 |
84 | .eth-processing {
85 | font-size: 0.9rem;
86 | font-style: italic;
87 | color: $primary-high;
88 | }
89 |
90 | .eth-success {
91 | i {
92 | color: $success;
93 | font-size: 6rem;
94 | }
95 | }
96 |
97 | .eth-transaction-id-label {
98 | font-weight: bold;
99 | font-size: 0.9rem;
100 | }
101 |
102 | .eth-transaction-id {
103 | color: $primary-high;
104 | font-weight: bold;
105 | font-size: 0.95rem;
106 | }
107 |
108 | .eth-success-message, .eth-ok-btn {
109 | margin-top: 1rem;
110 | }
111 |
--------------------------------------------------------------------------------
/assets/stylesheets/mobile.scss:
--------------------------------------------------------------------------------
1 | .user-main .user-profile-controls-outlet.discourse-ethereum {
2 | > span {
3 | margin: 0
4 | }
5 |
6 | button {
7 | width: 100%;
8 | margin-bottom: 10px;
9 | }
10 | }
11 |
12 | .eth-input {
13 | input {
14 | width: 150px;
15 | }
16 | }
17 |
18 | .eth-user {
19 | width: 115px;
20 | }
21 |
22 | .eth-transaction-id {
23 | max-width: 300px;
24 | margin: 0 auto;
25 | word-wrap: break-word;
26 | }
27 |
--------------------------------------------------------------------------------
/config/locales/client.en.yml:
--------------------------------------------------------------------------------
1 | en:
2 | js:
3 | discourse_ethereum:
4 | ethereum_address: "Ethereum address"
5 | send_ethereum: "Send ETH"
6 | send_eth_button: "Send ETH"
7 | send: "Send"
8 | enter_amount: "Enter the amount"
9 | balance: "Balance:"
10 | processing: "Processing transaction..."
11 | transaction_id: "Transaction Hash:"
12 | success_message: "You both will receive a PM containing the detail of the transaction."
13 | error_message: "An error has occured."
14 | ok: "OK"
15 |
--------------------------------------------------------------------------------
/config/locales/server.en.yml:
--------------------------------------------------------------------------------
1 | en:
2 | site_settings:
3 | discourse_ethereum_enabled: "Enable Ethereum plugin"
4 | discourse_ethereum_groups: "Enable Ethereum plugin only for member of these groups"
5 | discourse_ethereum_all_user: "Enable Ethereum plugin for all user"
6 | discourse_ethereum_erc20_token: "ERC20 token address"
7 | discourse_ethereum:
8 | ether: "Ether"
9 | pm_title: "Transaction %{hash}"
10 | pm_body: |
11 |
12 | %{table}
13 |
14 | pm_table:
15 | hash: "__Hash__"
16 | contract: "__Contract__"
17 | from: "__From__"
18 | to: "__To__"
19 | token_transfered: "__Token Transfered__"
20 | value: "__Value__"
21 | gas: "__Gas__"
22 | gas_price: "__Gas Price__"
23 |
--------------------------------------------------------------------------------
/config/settings.yml:
--------------------------------------------------------------------------------
1 | plugins:
2 | discourse_ethereum_enabled:
3 | default: true
4 | client: true
5 | discourse_ethereum_groups:
6 | type: "list"
7 | default: ""
8 | discourse_ethereum_all_user:
9 | default: true
10 | discourse_ethereum_erc20_token:
11 | default: ""
12 | client: true
13 |
--------------------------------------------------------------------------------
/jobs/send_tx_details.rb:
--------------------------------------------------------------------------------
1 | module Jobs
2 | class SendTxDetails < Jobs::Base
3 |
4 | sidekiq_options retry: false
5 |
6 | def execute(args)
7 | Ethereum::TxDetail.new(args).send_pm
8 | end
9 |
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/ethereum.rb:
--------------------------------------------------------------------------------
1 | module Ethereum
2 | class TxDetail
3 |
4 | def initialize(tx)
5 | @tx = tx
6 | end
7 |
8 | def send_pm
9 | opts = {
10 | title: title,
11 | archetype: Archetype.private_message,
12 | target_usernames: target_usernames,
13 | raw: t("pm_body", table: tx_table),
14 | skip_validations: true
15 | }
16 |
17 | creator = User.find(-1)
18 |
19 | PostCreator.create!(creator, opts)
20 | end
21 |
22 | private
23 |
24 | def title
25 | tx_hash = @tx["hash"].first(10) + "..." + @tx["hash"].last(5) # use same format with metamask
26 | t("pm_title", hash: tx_hash)
27 | end
28 |
29 | def tx_table
30 | tx_url = etherscan_url("tx", @tx["hash"])
31 | hash_td = tx_url ? "[#{@tx["hash"]}](#{tx_url})" : @tx["hash"]
32 | contract_td = @tx["token"] ? "[#{@tx["token"]}](#{etherscan_url("address", @tx["token"])})" : ""
33 | token_transfered_td = @tx["token_transfered"] ? "#{@tx["token_transfered"]} __#{@tx["symbol"]}__" : ""
34 | value_td = @tx["value"] ? "#{@tx["value"]} __#{@tx["symbol"]}__" : ""
35 |
36 | [
37 | "| | |",
38 | "|-|-|",
39 | "#{t('pm_table.hash')} | #{hash_td}",
40 | "#{t('pm_table.contract')} | #{contract_td}",
41 | "#{t('pm_table.from')} | #{username_and_address("from")}",
42 | "#{t('pm_table.to')} | #{username_and_address("to")}",
43 | "#{t('pm_table.token_transfered')} | #{token_transfered_td}",
44 | "#{t('pm_table.value')} | #{value_td}",
45 | "#{t('pm_table.gas')} | #{@tx["gas"]}",
46 | "#{t('pm_table.gas_price')} | #{@tx["gas_price"]}"
47 | ].join("\n")
48 | end
49 |
50 | def t(path, args = {})
51 | I18n.t("discourse_ethereum.#{path}", args)
52 | end
53 |
54 | def target_usernames
55 | ["from", "to"].map { |k| @tx.dig(k, "username") }
56 | end
57 |
58 | def username_and_address(key)
59 | address = @tx.dig(key, "address")
60 | url = user_etherscan_url(address)
61 | str = "@#{@tx.dig(key, "username")} "
62 |
63 | str += url ? "([#{address}](#{url}))" : "(address)"
64 |
65 | str
66 | end
67 |
68 | def user_etherscan_url(address)
69 | @tx["token"] ? etherscan_url("token", @tx["token"] + "?a=" + address) : etherscan_url("address", address)
70 | end
71 |
72 | def etherscan_url(path, address)
73 | return if @tx["net_prefix"].nil?
74 |
75 | "https://#{@tx["net_prefix"]}etherscan.io/#{path}/#{address}"
76 | end
77 |
78 | end
79 | end
80 |
--------------------------------------------------------------------------------
/plugin.rb:
--------------------------------------------------------------------------------
1 | # name: discourse-ethereum
2 | # version: 0.1.4
3 | # author: ProCourse Team
4 | # url: https://github.com/santiment/discourse-ethereum
5 |
6 | enabled_site_setting :discourse_ethereum_enabled
7 | register_asset "stylesheets/common.scss"
8 | register_asset "stylesheets/mobile.scss", :mobile
9 |
10 | require_relative "lib/ethereum"
11 |
12 | after_initialize {
13 |
14 | register_editable_user_custom_field("ethereum_address")
15 |
16 | load File.expand_path("../jobs/send_tx_details.rb", __FILE__)
17 |
18 | require_dependency "user"
19 | User.class_eval {
20 | def eth_enabled?
21 | !suspended? &&
22 | SiteSetting.discourse_ethereum_enabled &&
23 | (SiteSetting.discourse_ethereum_all_user ||
24 | self.groups.where(name: SiteSetting.discourse_ethereum_groups.split("|")).exists?)
25 | end
26 | }
27 |
28 | require_dependency "guardian"
29 | Guardian.class_eval {
30 |
31 | def can_do_eth_transaction?(target_user)
32 | return false unless authenticated?
33 |
34 | current_user&.eth_enabled? &&
35 | target_user&.eth_enabled? &&
36 | target_user.custom_fields["ethereum_address"].present?
37 | end
38 |
39 | }
40 |
41 | add_to_serializer(:user, :can_do_eth_transaction) {
42 | scope.can_do_eth_transaction?(object)
43 | }
44 |
45 | add_to_serializer(:user, :ethereum_address) {
46 | if scope.user&.eth_enabled? && eth_enabled_for_user
47 | object.custom_fields["ethereum_address"].to_s.downcase
48 | end
49 | }
50 |
51 | add_to_serializer(:user, :eth_enabled_for_user) {
52 | @eth_enabled_for_user ||= object&.eth_enabled?
53 | }
54 |
55 | require_dependency "application_controller"
56 | class ::EthereumController < ::ApplicationController
57 | requires_plugin("discourse-ethereum")
58 | before_action :ensure_logged_in
59 |
60 | def send_tx_details
61 | tx = params.require(:tx)
62 |
63 | Jobs.enqueue(:send_tx_details, tx.to_unsafe_hash)
64 |
65 | render json: success_json
66 | end
67 | end
68 |
69 | Discourse::Application.routes.append {
70 |
71 | post "ethereum" => "ethereum#send_tx_details"
72 |
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/screenshot/pm-tx-details-erc20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/santiment/discourse-ethereum/4ef56dc5d8c50d4dac28eaa42dae80492d6451b3/screenshot/pm-tx-details-erc20.png
--------------------------------------------------------------------------------
/screenshot/pm-tx-details-eth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/santiment/discourse-ethereum/4ef56dc5d8c50d4dac28eaa42dae80492d6451b3/screenshot/pm-tx-details-eth.png
--------------------------------------------------------------------------------
/screenshot/send-eth-modal-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/santiment/discourse-ethereum/4ef56dc5d8c50d4dac28eaa42dae80492d6451b3/screenshot/send-eth-modal-1.png
--------------------------------------------------------------------------------
/screenshot/send-eth-modal-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/santiment/discourse-ethereum/4ef56dc5d8c50d4dac28eaa42dae80492d6451b3/screenshot/send-eth-modal-2.png
--------------------------------------------------------------------------------
/screenshot/send-eth-modal-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/santiment/discourse-ethereum/4ef56dc5d8c50d4dac28eaa42dae80492d6451b3/screenshot/send-eth-modal-3.png
--------------------------------------------------------------------------------
/screenshot/send-eth-user-card.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/santiment/discourse-ethereum/4ef56dc5d8c50d4dac28eaa42dae80492d6451b3/screenshot/send-eth-user-card.png
--------------------------------------------------------------------------------
/screenshot/send-eth-user-profile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/santiment/discourse-ethereum/4ef56dc5d8c50d4dac28eaa42dae80492d6451b3/screenshot/send-eth-user-profile.png
--------------------------------------------------------------------------------