├── 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 | ![](screenshot/send-eth-user-card.png) 11 | ![](screenshot/send-eth-user-profile.png) 12 | 13 | ### 'Send ETH' Modal 14 | 15 | ![](screenshot/send-eth-modal-1.png) 16 | ![](screenshot/send-eth-modal-2.png) 17 | ![](screenshot/send-eth-modal-3.png) 18 | 19 | ### PM 20 | PM containing the transaction details sent by `system` to both user: 21 | 22 | #### Transfer ETH 23 | ![pm](screenshot/pm-tx-details-eth.png) 24 | 25 | #### Transfer ERC20 Token 26 | ![pm](screenshot/pm-tx-details-erc20.png) 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 | 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 |
33 |
34 | {{#if formatedBalance}} 35 | {{i18n "discourse_ethereum.balance"}} 36 | {{formatedBalance}} {{symbol}} 37 | {{/if}} 38 |
39 | 40 |
41 | {{input 42 | type="number" 43 | value=amount 44 | placeholder=(i18n "discourse_ethereum.enter_amount") 45 | min=0 46 | max=balance 47 | step="0.001" 48 | }} 49 | 50 | {{#if erc20enabled}} 51 | 56 | {{/if}} 57 |
58 | 59 |
60 | {{d-button 61 | disabled=isDisabled 62 | action="send" 63 | class="btn-large btn-primary" 64 | label="discourse_ethereum.send" 65 | }} 66 |
67 |
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 --------------------------------------------------------------------------------