[refactor] remove ethereum `call` and `call-params`

- use `json-rpc/eth-call` and `json-rpc/eth-transaction-call`
everywhere
- move all conversions to abi-spec
This commit is contained in:
yenda 2019-05-20 02:21:38 +02:00
parent fc4c772c0b
commit b6fecd4e1c
No known key found for this signature in database
GPG Key ID: 0095623C0069DCE6
18 changed files with 502 additions and 476 deletions

View File

@ -1,61 +1,17 @@
(ns status-im.ethereum.contracts
(:require [re-frame.core :as re-frame]
[status-im.utils.ethereum.abi-spec :as abi-spec]
[status-im.utils.ethereum.core :as ethereum]
[status-im.utils.fx :as fx]
[status-im.utils.money :as money]
[status-im.wallet.core :as wallet]))
(:require [status-im.utils.ethereum.core :as ethereum]))
(def contracts
{:status/tribute-to-talk
{:address
{:mainnet nil
:testnet "0x3da3fc53e24707f36c5b4433b442e896c4955f0e"
:rinkeby nil}
:methods
{:get-manifest
{:signature "getManifest(address)"
:outputs ["bytes"]}
:set-manifest
{:signature "setManifest(bytes)"
:write? true}}}})
{:status/snt
{:mainnet "0x744d70fdbe2ba4cf95131626614a1763df805b9e"
:testnet "0xc55cf4b03948d7ebc8b9e8bad92643703811d162"}
:status/tribute-to-talk
{:testnet "0x3da3fc53e24707f36c5b4433b442e896c4955f0e"}
:status/stickers
{:testnet "0x39d16CdB56b5a6a89e1A397A13Fe48034694316E"}})
(re-frame/reg-fx
::call
(fn [{:keys [address data callback]}]
(ethereum/call {:to address
:data data}
callback)))
(defn get-contract-address
(defn get-address
[db contract]
(let [chain-keyword (-> (get-in db [:account/account :networks (:network db)])
ethereum/network->chain-keyword)]
(get-in contracts [contract :address chain-keyword])))
(fx/defn call
[{:keys [db] :as cofx}
{:keys [contract contract-address method params
callback on-result on-error details]}]
(when-let [contract-address (or contract-address
(get-contract-address db contract))]
(let [{:keys [signature outputs write?]}
(get-in contracts [contract :methods method])
data (abi-spec/encode signature params)]
(if write?
(wallet/open-sign-transaction-flow
cofx
(merge {:to contract-address
:data data
:id "approve"
:symbol :ETH
:method "eth_sendTransaction"
:amount (money/bignumber 0)
:on-result on-result
:on-error on-error}
details))
{::call {:address contract-address
:data data
:callback #(callback (if (empty? outputs)
%
(abi-spec/decode % outputs)))}}))))
(get-in contracts [contract chain-keyword])))

View File

@ -3,22 +3,94 @@
[re-frame.core :as re-frame]
[status-im.accounts.core :as accounts]
[status-im.constants :as constants]
[status-im.ethereum.contracts :as contracts]
[status-im.ethereum.json-rpc :as json-rpc]
[status-im.ui.screens.navigation :as navigation]
[status-im.utils.ethereum.abi-spec :as abi-spec]
[status-im.utils.ethereum.core :as ethereum]
[status-im.utils.ethereum.stickers :as ethereum.stickers]
[status-im.utils.fx :as fx]
[status-im.utils.money :as money]
[status-im.utils.multihash :as multihash]
[status-im.utils.utils :as utils]
[status-im.wallet.core :as wallet]))
(fx/defn init-stickers-packs [{:keys [db]}]
(defn pack-data-callback
[id open?]
(fn [[category owner mintable timestamp price contenthash]]
(let [proto-code (subs contenthash 2 4)
hash (when contenthash
(multihash/base58 (multihash/create :sha2-256 (subs contenthash 12))))]
(when (and (#{constants/swarm-proto-code constants/ipfs-proto-code}
proto-code) hash)
(re-frame/dispatch [:stickers/load-pack proto-code hash id price open?])))))
(re-frame/reg-fx
:stickers/set-pending-timout-fx
(fn []
(utils/set-timeout #(re-frame/dispatch [:stickers/pending-timout])
10000)))
(defn eth-call-pack-data
[contract id open?]
(json-rpc/eth-call
{:contract contract
;; Returns vector of pack data parameters by pack id:
;; [category owner mintable timestamp price contenthash]
:method "getPackData(uint256)"
:params [id]
:outputs ["bytes4[]" "address" "bool" "uint256" "uint256" "bytes"]
:on-success (pack-data-callback id open?)}))
(re-frame/reg-fx
:stickers/pack-data-fx
(fn [[contract id]]
(eth-call-pack-data contract id true)))
(re-frame/reg-fx
:stickers/load-packs-fx
(fn [[contract]]
(json-rpc/eth-call
{:contract contract
;; Returns number of packs registered in the contract
:method "packCount()"
:outputs ["uint256"]
:on-success
(fn [[count]]
(dotimes [id count]
(eth-call-pack-data contract id false)))})))
(re-frame/reg-fx
:stickers/owned-packs-fx
(fn [[contract address]]
(json-rpc/eth-call
{:contract contract
;; Returns vector of owned tokens ids in the contract by address
:method "tokensOwnedBy(address)"
:params [address]
:outputs ["uint256[]"]
:on-success
(fn [[tokens]]
(doseq [id tokens]
(json-rpc/eth-call
{:contract contract
;; Returns pack id in the contract by token id
:method "tokenPackId(uint256)"
:params [id]
:outputs ["uint256"]
:on-success
(fn [[pack-id]]
(re-frame/dispatch [:stickers/pack-owned pack-id]))})))})))
(fx/defn init-stickers-packs
[{:keys [db]}]
(let [sticker-packs (into {} (map #(let [pack (edn/read-string %)]
(vector (:id pack) pack))
(get-in db [:account/account :stickers])))]
{:db (assoc db :stickers/packs-installed sticker-packs :stickers/packs sticker-packs)}))
{:db (assoc db
:stickers/packs-installed sticker-packs
:stickers/packs sticker-packs)}))
(fx/defn install-stickers-pack [{{:account/keys [account] :as db} :db :as cofx} id]
(fx/defn install-stickers-pack
[{{:account/keys [account] :as db} :db :as cofx} id]
(let [pack (get-in db [:stickers/packs id])]
(fx/merge
cofx
@ -27,125 +99,91 @@
(assoc :stickers/selected-pack id))}
(accounts/update-stickers (conj (:stickers account) (pr-str pack))))))
(fx/defn load-sticker-pack-success [{:keys [db] :as cofx} edn-string id price open?]
(fx/defn load-sticker-pack-success
[{:keys [db] :as cofx} edn-string id price open?]
(let [pack (assoc (get (edn/read-string edn-string) 'meta)
:id id :price price)]
(fx/merge cofx
{:db (-> db (assoc-in [:stickers/packs id] pack))}
#(when open? (navigation/navigate-to-cofx % :stickers-pack-modal pack)))))
(defn pack-data-callback [id open?]
(fn [[category owner mintable timestamp price contenthash]]
(let [proto-code (subs contenthash 2 4)
hash (when contenthash (multihash/base58 (multihash/create :sha2-256 (subs contenthash 12))))]
(when (and (#{constants/swarm-proto-code constants/ipfs-proto-code} proto-code) hash)
(re-frame/dispatch [:stickers/load-pack proto-code hash id price open?])))))
{:db (assoc-in db [:stickers/packs id] pack)}
#(when open?
(navigation/navigate-to-cofx % :stickers-pack-modal pack)))))
(fx/defn open-sticker-pack
[{{:keys [network] :stickers/keys [packs packs-installed] :as db} :db :as cofx} id]
[{{:stickers/keys [packs packs-installed] :as db} :db :as cofx} id]
(when id
(let [pack (or (get packs-installed id) (get packs id))
network (get-in db [:account/account :networks network])]
(let [pack (or (get packs-installed id)
(get packs id))
contract-address (contracts/get-address db :status/stickers)]
(if pack
(navigation/navigate-to-cofx cofx :stickers-pack-modal pack)
{:stickers/pack-data-fx [network id true]}))))
(when contract-address
{:stickers/pack-data-fx [contract-address id]})))))
(fx/defn load-pack [cofx proto-code hash id price open?]
(fx/defn load-pack
[cofx proto-code hash id price open?]
{:http-get {:url (str (if (= constants/swarm-proto-code proto-code)
"https://swarm-gateways.net/bzz:/"
"https://ipfs.infura.io/ipfs/")
hash)
:success-event-creator (fn [o]
:success-event-creator
(fn [o]
[:stickers/load-sticker-pack-success o id price open?])
:failure-event-creator (constantly nil)}})
:failure-event-creator
(constantly nil)}})
(fx/defn load-packs [{{:keys [network] :as db} :db}]
(let [network (get-in db [:account/account :networks network])
address (ethereum/normalized-address (get-in db [:account/account :address]))]
{:stickers/owned-packs-fx [network address]
:stickers/load-packs-fx [network]}))
(fx/defn load-packs
[{:keys [db]}]
(let [contract (contracts/get-address db :status/stickers)
address (ethereum/current-address db)]
(when contract
{:stickers/owned-packs-fx [contract address]
:stickers/load-packs-fx [contract]})))
(defn prepare-transaction [id tx on-result]
(merge {:id id
:symbol :ETH
:method constants/web3-send-transaction
:amount (money/bignumber 0)}
(when on-result {:on-result on-result})
tx))
(def snt-contracts
{:mainnet "0x744d70fdbe2ba4cf95131626614a1763df805b9e"
:testnet "0xc55cf4b03948d7ebc8b9e8bad92643703811d162"
:rinkeby nil})
(fx/defn approve-pack [{db :db} pack-id price]
(let [network (get-in db [:account/account :networks (:network db)])
address (ethereum/normalized-address (get-in db [:account/account :address]))
chain (ethereum/network->chain-keyword network)
stickers-contract (get ethereum.stickers/contracts chain)
data (abi-spec/encode "buyToken(uint256,address)" [pack-id address])
tx-object {:to (get snt-contracts chain)
:data (abi-spec/encode "approveAndCall(address,uint256,bytes)" [stickers-contract price data])}]
(wallet/open-modal-wallet-for-transaction
db
(prepare-transaction "approve" tx-object [:stickers/pending-pack pack-id])
tx-object)))
(fx/defn approve-pack
[{db :db :as cofx} pack-id price]
(let [address (ethereum/current-address db)
chain (ethereum/chain-keyword db)
stickers-contract (contracts/get-address db :status/stickers)
snt-contract (contracts/get-address db :status/snt)]
(wallet/eth-transaction-call
cofx
{:contract snt-contract
:method "approveAndCall(address,uint256,bytes)"
:params [stickers-contract
price
(abi-spec/encode "buyToken(uint256,address)"
[pack-id address])]
:on-result [:stickers/pending-pack pack-id]})))
(fx/defn pending-pack
[{{:keys [network] :as db} :db :as cofx} id]
(let [network (get-in db [:account/account :networks network])
address (ethereum/normalized-address (get-in db [:account/account :address]))]
[{:keys [db] :as cofx} id]
(let [contract (contracts/get-address db :status/stickers)
address (ethereum/current-address db)]
(when contract
(fx/merge cofx
{:db (update db :stickers/packs-pendning conj id)
:stickers/owned-packs-fx [network address]}
{:db (update db :stickers/packs-pending conj id)
:stickers/owned-packs-fx [contract address]}
(navigation/navigate-to-clean :wallet-transaction-sent-modal {})
#(when (zero? (count (:stickers/packs-pendning db)))
{:stickers/set-pending-timout-fx nil}))))
#(when (zero? (count (:stickers/packs-pending db)))
{:stickers/set-pending-timout-fx nil})))))
(fx/defn pending-timeout
[{{:keys [network] :stickers/keys [packs-pendning packs-owned] :as db} :db}]
(let [packs-diff (clojure.set/difference packs-pendning packs-owned)
network (get-in db [:account/account :networks network])
address (ethereum/normalized-address (get-in db [:account/account :address]))]
(merge {:db (assoc db :stickers/packs-pendning packs-diff)}
[{{:stickers/keys [packs-pending packs-owned] :as db} :db}]
(let [packs-diff (clojure.set/difference packs-pending packs-owned)
contract (contracts/get-address db :status/stickers)
address (ethereum/current-address db)]
(when contract
(merge {:db (assoc db :stickers/packs-pending packs-diff)}
(when-not (zero? (count packs-diff))
{:stickers/owned-packs-fx [network address]
:stickers/set-pending-timout-fx nil}))))
{:stickers/owned-packs-fx [contract address]
:stickers/set-pending-timout-fx nil})))))
(fx/defn pack-owned [{db :db} id]
{:db (update db :stickers/packs-owned conj id)})
(fx/defn get-owned-pack
[{{:keys [network] :as db} :db}]
(let [address (ethereum/normalized-address (get-in db [:account/account :address]))]
{:stickers/owned-packs-fx [network address]}))
(re-frame/reg-fx
:stickers/pack-data-fx
(fn [[network id open?]]
(when-let [contract (get ethereum.stickers/contracts (ethereum/network->chain-keyword network))]
(ethereum.stickers/pack-data contract id (pack-data-callback id open?)))))
(re-frame/reg-fx
:stickers/set-pending-timout-fx
(fn []
(js/setTimeout #(re-frame/dispatch [:stickers/pending-timout]) 10000)))
(re-frame/reg-fx
:stickers/load-packs-fx
(fn [[network]]
(when-let [contract (get ethereum.stickers/contracts (ethereum/network->chain-keyword network))]
(ethereum.stickers/pack-count contract
(fn [count]
(dotimes [n count]
(ethereum.stickers/pack-data contract n (pack-data-callback n false))))))))
(re-frame/reg-fx
:stickers/owned-packs-fx
(fn [[network address]]
(when-let [contract (get ethereum.stickers/contracts (ethereum/network->chain-keyword network))]
(ethereum.stickers/owned-tokens contract address
(fn [tokens]
(doseq [n tokens]
(ethereum.stickers/token-pack-id contract n
#(re-frame/dispatch [:stickers/pack-owned %]))))))))
[{:keys [db]}]
(let [contract (contracts/get-address db :status/stickers)
address (ethereum/current-address db)]
(when contract
{:stickers/owned-packs-fx [contract address]})))

View File

@ -126,7 +126,7 @@
(reg-root-key-sub :stickers/packs :stickers/packs)
(reg-root-key-sub :stickers/installed-packs :stickers/packs-installed)
(reg-root-key-sub :stickers/packs-owned :stickers/packs-owned)
(reg-root-key-sub :stickers/packs-pendning :stickers/packs-pendning)
(reg-root-key-sub :stickers/packs-pending :stickers/packs-pending)
;;mailserver
(reg-root-key-sub :mailserver/current-id :mailserver/current-id)
@ -768,7 +768,7 @@
:<- [:stickers/packs]
:<- [:stickers/installed-packs]
:<- [:stickers/packs-owned]
:<- [:stickers/packs-pendning]
:<- [:stickers/packs-pending]
(fn [[packs installed owned pending]]
(map (fn [{:keys [id] :as pack}]
(cond-> pack
@ -1167,9 +1167,8 @@
(let [{:keys [gas-used gas-price hash timestamp type token value]
:as transaction}
(get transactions current-transaction)
native-currency-text (-> native-currency
:symbol-display
name)]
native-currency-text (name (or (:symbol-display native-currency)
(:symbol native-currency)))]
(when transaction
(merge transaction
{:gas-price-eth (if gas-price

View File

@ -5,12 +5,14 @@
[status-im.accounts.update.core :as accounts.update]
[status-im.contact.db :as contact.db]
[status-im.ethereum.contracts :as contracts]
[status-im.ethereum.json-rpc :as json-rpc]
[status-im.ipfs.core :as ipfs]
[status-im.tribute-to-talk.db :as tribute-to-talk.db]
[status-im.ui.screens.navigation :as navigation]
[status-im.utils.contenthash :as contenthash]
[status-im.utils.ethereum.core :as ethereum]
[status-im.utils.fx :as fx]
[status-im.wallet.core :as wallet]
[taoensso.timbre :as log]))
(fx/defn update-settings
@ -195,21 +197,31 @@
[:tribute-to-talk.callback/fetch-manifest-success
identity manifest])))}))
(re-frame/reg-fx
:tribute-to-talk/get-manifest
(fn [{:keys [contract address on-success]}]
(json-rpc/eth-call
{:contract contract
:method "getManifest(address)"
:params [address]
:outputs ["bytes"]
:on-success on-success})))
(fx/defn check-manifest
[{:keys [db] :as cofx} identity]
(or (contracts/call cofx
{:contract :status/tribute-to-talk
:method :get-manifest
:params [(contact.db/public-key->address identity)]
:return-params ["bytes"]
:callback
#(re-frame/dispatch
(if-let [contenthash (first %)]
[{:keys [db] :as cofx} public-key]
(if-let [contract (contracts/get-address db :status/tribute-to-talk)]
(let [address (contact.db/public-key->address public-key)]
{:tribute-to-talk/get-manifest
{:contract contract
:address address
:on-success
(fn [[contenthash]]
(re-frame/dispatch
(if contenthash
[:tribute-to-talk.callback/check-manifest-success
identity
public-key
contenthash]
[:tribute-to-talk.callback/no-manifest-found identity]))})
;; `contracts/call` returns nil if there is no contract for the current network
[:tribute-to-talk.callback/no-manifest-found public-key])))}})
;; update settings if checking own manifest or do nothing otherwise
(when-let [me? (= identity
(get-in cofx [:db :account/account :public-key]))]
@ -224,13 +236,17 @@
(let [contenthash (when hash
(contenthash/encode {:hash hash
:namespace :ipfs}))]
(or (contracts/call cofx
{:contract :status/tribute-to-talk
:method :set-manifest
(if-let [contract (contracts/get-address db :status/tribute-to-talk)]
(wallet/eth-transaction-call
cofx
{:contract contract
:method "setManifest(bytes)"
:params [contenthash]
:on-result [:tribute-to-talk.callback/set-manifest-transaction-completed]
:on-error [:tribute-to-talk.callback/set-manifest-transaction-failed]})
{:db (assoc-in db [:navigation/screen-params :tribute-to-talk :state] :transaction-failed)})))
{:db (assoc-in db
[:navigation/screen-params :tribute-to-talk :state]
:transaction-failed)})))
(defn remove
[{:keys [db] :as cofx}]

View File

@ -14,15 +14,14 @@
(handlers/register-handler-fx
:new-chat/set-new-identity
(fn [{{:keys [network network-status] :as db} :db} [_ new-identity]]
(fn [{{:keys [network-status] :as db} :db} [_ new-identity]]
(let [is-public-key? (and (string? new-identity)
(string/starts-with? new-identity "0x"))]
(merge {:db (assoc db
:contacts/new-identity new-identity
:contacts/new-identity-error (db/validate-pub-key db new-identity))}
(when-not is-public-key?
(let [network (get-in db [:account/account :networks network])
chain (ethereum/network->chain-keyword network)]
(let [chain (ethereum/chain-keyword db)]
{:resolve-public-key {:registry (get ens/ens-registries chain)
:ens-name (if (ens/is-valid-eth-name? new-identity)
new-identity

View File

@ -93,7 +93,8 @@
[stickers-panel (map #(assoc % :pack id) stickers) window-width])]))
(defn pack-icon [{:keys [id on-press background-color]
:or {on-press #(re-frame/dispatch [:stickers/select-pack id])}} icon]
:or {on-press #(re-frame/dispatch [:stickers/select-pack id])}}
icon]
[react/touchable-highlight {:on-press on-press}
[react/view {:style {:align-items :center}}
[react/view {:style (styles/pack-icon background-color icon-size icon-horizontal-margin)}
@ -148,7 +149,8 @@
[vector-icons/icon :stickers-icons/recent {:color colors/gray}]]
(for [{:keys [id thumbnail]} installed-packs]
^{:key id}
[pack-icon {:id id}
[pack-icon {:id id
:background-color colors/white}
[react/image {:style {:width icon-size :height icon-size :border-radius (/ icon-size 2)}
:source {:uri thumbnail}}]])]
[scroll-indicator]]]]]))

View File

@ -61,7 +61,7 @@
:push-notifications/stored {}
:registry {}
:stickers/packs-owned #{}
:stickers/packs-pendning #{}
:stickers/packs-pending #{}
:hardwallet {:nfc-supported? false
:nfc-enabled? false
:pin {:original []
@ -192,7 +192,7 @@
(spec/def :stickers/packs (spec/nilable map?))
(spec/def :stickers/packs-owned (spec/nilable set?))
(spec/def :stickers/packs-pendning (spec/nilable set?))
(spec/def :stickers/packs-pending (spec/nilable set?))
(spec/def :stickers/packs-installed (spec/nilable map?))
(spec/def :stickers/selected-pack (spec/nilable any?))
(spec/def :stickers/recent (spec/nilable vector?))
@ -269,7 +269,7 @@
:stickers/selected-pack
:stickers/recent
:stickers/packs-owned
:stickers/packs-pendning
:stickers/packs-pending
:bottom-sheet/show?
:bottom-sheet/view
:bottom-sheet/options

View File

@ -6,6 +6,7 @@
[status-im.native-module.core :as status]
[status-im.transport.utils :as transport.utils]
[status-im.ui.screens.navigation :as navigation]
[status-im.utils.ethereum.abi-spec :as abi-spec]
[status-im.utils.ethereum.core :as ethereum]
[status-im.utils.ethereum.tokens :as tokens]
[status-im.utils.fx :as fx]
@ -29,10 +30,13 @@
{:keys [from to value gas gasPrice]} on-completed masked-password]
(let [contract (:address (tokens/symbol->token all-tokens (keyword chain) symbol))]
(status/send-transaction (types/clj->json
(merge (ethereum/call-params contract "transfer(address,uint256)" to value)
{:from from
{:to contract
:from from
:data (abi-spec/encode
"transfer(address,uint256)"
[to value])
:gas gas
:gasPrice gasPrice}))
:gasPrice gasPrice})
(security/safe-unmask-data masked-password)
on-completed)))
@ -66,9 +70,10 @@
(handlers/register-handler-fx
:wallet/send-transaction
(fn [{{:keys [chain] :as db} :db} _]
(let [{:keys [password symbol in-progress?] :as transaction} (get-in db [:wallet :send-transaction])
(let [{:keys [password symbol in-progress?] :as transaction}
(get-in db [:wallet :send-transaction])
all-tokens (:wallet/all-tokens db)
from (get-in db [:account/account :address])]
from (ethereum/current-address db)]
(when-not in-progress?
{:db (-> db
(assoc-in [:wallet :send-transaction :wrong-password?] false)
@ -374,8 +379,9 @@
(wallet/prepare-send-transaction from transaction)
(let [contract (:address (tokens/symbol->token all-tokens (keyword chain) symbol))
{:keys [gas gasPrice to from value]} (wallet/prepare-send-transaction from transaction)]
(merge (ethereum/call-params contract "transfer(address,uint256)" to value)
{:from from
(merge (abi-spec/encode "transfer(address,uint256)" [to value])
{:to contract
:from from
:gas gas
:gasPrice gasPrice}))))

View File

@ -28,6 +28,9 @@
(when x
(subs (.fromUtf8 utils x) 2)))
(defn hex-to-boolean [x]
(= x "0x0"))
(defn bytes-to-hex [x]
(when x
(subs (.bytesToHex utils x) 2)))

View File

@ -1,6 +1,5 @@
(ns status-im.utils.ethereum.core
(:require [clojure.string :as string]
[status-im.ethereum.json-rpc :as json-rpc]
[status-im.js-dependencies :as dependencies]
[status-im.utils.ethereum.tokens :as tokens]
[status-im.utils.money :as money]))
@ -40,6 +39,10 @@
address
(str hex-prefix address))))
(defn current-address [db]
(-> (get-in db [:account/account :address])
normalized-address))
(defn naked-address [s]
(when s
(string/replace s hex-prefix "")))
@ -74,71 +77,6 @@
([s opts]
(.sha3 dependencies/Web3.prototype (str s) (clj->js opts))))
(defn hex->string [s]
(when s
(let [hex (.toString s)]
(loop [res "" i (if (string/starts-with? hex hex-prefix) 2 0)]
(if (and (< i (.-length hex)))
(recur
(if (= (.substr hex i 2) "00")
res
(str res (.fromCharCode js/String (js/parseInt (.substr hex i 2) 16))))
(+ i 2))
res)))))
(defn hex->boolean [s]
(= s "0x0"))
(defn boolean->hex [b]
(if b "0x0" "0x1"))
(defn hex->int [s]
(if (= s hex-prefix)
0
(js/parseInt s 16)))
(defn int->hex [i]
(.toHex dependencies/Web3.prototype i))
(defn hex->bignumber [s]
(money/bignumber (if (= s hex-prefix) 0 s)))
(defn hex->address
"When hex value is 66 char in length (2 for 0x, 64 for
the 32 bytes used by abi-spec for an address), only keep
the part that constitute the address and normalize it,"
[s]
(when (= 66 (count s))
(normalized-address (subs s 26))))
(defn zero-pad-64 [s]
(str (apply str (drop (count s) (repeat 64 "0"))) s))
(defn string->hex [i]
(.fromAscii dependencies/Web3.prototype i))
(defn format-param [param]
(if (number? param)
(zero-pad-64 (str (hex->int param)))
(zero-pad-64 (subs param 2))))
(defn format-call-params [method-id & params]
(let [params (string/join (map format-param params))]
(str method-id params)))
(defn- sig->method-id [signature]
(apply str (take 10 (sha3 signature))))
(defn call [params callback]
(json-rpc/call
{:method "eth_call"
:params [params "latest"]
:on-success callback}))
(defn call-params [contract method-sig & params]
(let [data (apply format-call-params (sig->method-id method-sig) params)]
{:to contract :data data}))
(def default-transaction-gas (money/bignumber 21000))
(defn estimate-gas [symbol]

View File

@ -1,13 +1,18 @@
(ns status-im.utils.ethereum.eip165
"Utility function related to [EIP165](https://eips.ethereum.org/EIPS/eip-165)"
(:require [status-im.utils.ethereum.core :as ethereum]))
(:require [status-im.ethereum.json-rpc :as json-rpc]
[status-im.utils.ethereum.abi-spec :as abi-spec]))
(def supports-interface-hash "0x01ffc9a7")
(def marker-hash "0xffffffff")
(defn supports-interface? [contract hash cb]
(ethereum/call (ethereum/call-params contract "supportsInterface(bytes4)" hash)
#(cb %)))
(defn supports-interface?
[contract hash cb]
(json-rpc/eth-call
{:contract contract
:method "supportsInterface(bytes4)"
:params [hash]
:on-success cb}))
(defn supports?
"Calls cb with true if `supportsInterface` is supported by this contract.
@ -16,9 +21,9 @@
(supports-interface?
contract
supports-interface-hash
#(if (true? (ethereum/hex->boolean %))
#(if (true? (abi-spec/hex-to-boolean %))
(supports-interface? contract
marker-hash
(fn [response]
(cb (false? (ethereum/hex->boolean response)))))
(cb (false? (abi-spec/hex-to-boolean response)))))
(cb false))))

View File

@ -6,8 +6,8 @@
"
(:refer-clojure :exclude [name])
(:require [clojure.string :as string]
[status-im.utils.ethereum.core :as ethereum]
[status-im.utils.ethereum.abi-spec :as abi-spec]))
[status-im.ethereum.json-rpc :as json-rpc]
[status-im.utils.ethereum.core :as ethereum]))
;; this is the addresses of ens registries for the different networks
(def ens-registries
@ -17,7 +17,7 @@
(def default-namehash "0000000000000000000000000000000000000000000000000000000000000000")
(def default-address "0x0000000000000000000000000000000000000000")
(def default-key "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
(def default-key "0x0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
(defn namehash
[s]
@ -35,28 +35,37 @@
(defn resolver
[registry ens-name cb]
(ethereum/call (ethereum/call-params registry
"resolver(bytes32)"
(namehash ens-name))
(fn [address]
(let [address (ethereum/hex->address address)]
(cb (if (and address (not= address default-address)) address ""))))))
(json-rpc/eth-call
{:contract registry
:method "resolver(bytes32)"
:params [(namehash ens-name)]
:outputs ["address"]
:on-success
(fn [[address]]
(when-not (= address default-address)
(cb address)))}))
(defn owner
[registry ens-name cb]
(ethereum/call (ethereum/call-params registry
"owner(bytes32)"
(namehash ens-name))
(fn [address]
(cb address))))
(json-rpc/eth-call
{:contract registry
:method "owner(bytes32)"
:params [(namehash ens-name)]
:outputs ["address"]
:on-success
(fn [[address]]
(cb address))}))
(defn ttl
[registry ens-name cb]
(ethereum/call (ethereum/call-params registry
"ttl(bytes32)"
(namehash ens-name))
(fn [ttl]
(cb (ethereum/hex->int ttl)))))
(json-rpc/eth-call
{:contract registry
:method "ttl(bytes32)"
:params [(namehash ens-name)]
:outputs ["uint256"]
:on-success
(fn [[ttl]]
(cb ttl))}))
;; Resolver contract
;; Resolver must implement EIP65 (supportsInterface). When interacting with an unknown resolver it's safer to rely on it.
@ -65,9 +74,14 @@
(defn addr
[resolver ens-name cb]
(ethereum/call (ethereum/call-params resolver "addr(bytes32)" (namehash ens-name))
(fn [address]
(cb (ethereum/hex->address address)))))
(json-rpc/eth-call
{:contract resolver
:method "addr(bytes32)"
:params [(namehash ens-name)]
:outputs ["address"]
:on-success
(fn [[address]]
(cb address))}))
(def name-hash "0x691f3431")
@ -75,37 +89,41 @@
(defn name
[resolver ens-name cb]
(ethereum/call (ethereum/call-params resolver
"name(bytes32)"
(namehash ens-name))
(fn [address]
(cb (ethereum/hex->address address)))))
(json-rpc/eth-call
{:contract resolver
:method "name(bytes32)"
:params [(namehash ens-name)]
:outputs ["string"]
:on-success
(fn [[name]]
(cb name))}))
(defn contenthash
[resolver ens-name cb]
(ethereum/call (ethereum/call-params resolver
"contenthash(bytes32)"
(namehash ens-name))
(json-rpc/eth-call
{:contract resolver
:method "contenthash(bytes32)"
:params [(namehash ens-name)]
:on-success
(fn [hash]
(cb (first (abi-spec/decode hash ["bytes"]))))))
(cb hash))}))
(defn content
[resolver ens-name cb]
(ethereum/call (ethereum/call-params resolver
"content(bytes32)"
(namehash ens-name))
(json-rpc/eth-call
{:contract resolver
:method "content(bytes32)"
:params [(namehash ens-name)]
:on-success
(fn [hash]
(cb hash))))
(cb hash))}))
(def ABI-hash "0x2203ab56")
(def pubkey-hash "0xc8690233")
(defn add-uncompressed-public-key-prefix
[key]
(when (and key
(not= "0x" key)
(not= default-key key))
(str "0x04" (subs key 2))))
(defn uncompressed-public-key
[x y]
(str "0x04" x y))
(defn is-valid-eth-name?
[ens-name]
@ -115,12 +133,16 @@
(defn pubkey
[resolver ens-name cb]
(ethereum/call (ethereum/call-params resolver
"pubkey(bytes32)"
(namehash ens-name))
(fn [key]
(when-let [public-key (add-uncompressed-public-key-prefix key)]
(cb public-key)))))
(json-rpc/eth-call
{:contract resolver
:method "pubkey(bytes32)"
:params [(namehash ens-name)]
:outputs ["bytes32" "bytes32"]
:on-success
(fn [[x y]]
(when-let [public-key (uncompressed-public-key x y)]
(when-not (= public-key default-key)
(cb public-key))))}))
(defn get-addr
[registry ens-name cb]

View File

@ -2,19 +2,22 @@
"
Helper functions to interact with [ERC721](https://eips.ethereum.org/EIPS/eip-721) smart contract
"
(:require [status-im.utils.ethereum.core :as ethereum]))
(:require [status-im.ethereum.json-rpc :as json-rpc]))
(defn token-of-owner-by-index [contract address index cb]
(ethereum/call (ethereum/call-params
contract
"tokenOfOwnerByIndex(address,uint256)"
(ethereum/normalized-address address)
(ethereum/int->hex index))
#(cb (ethereum/hex->bignumber %))))
(defn token-of-owner-by-index
[contract address index cb]
(json-rpc/eth-call
{:contract contract
:method "tokenOfOwnerByIndex(address,uint256)"
:params [address index]
:outputs ["uint256"]
:on-success (fn [[token]] (cb token))}))
(defn token-uri [contract tokenId cb]
(ethereum/call (ethereum/call-params
contract
"tokenURI(uint256)"
(ethereum/int->hex tokenId))
#(cb (ethereum/hex->string %))))
(defn token-uri
[contract tokenId cb]
(json-rpc/eth-call
{:contract contract
:method "tokenURI(uint256)"
:params [tokenId]
:outputs ["string"]
:on-success (fn [[uri]] (cb uri))}))

View File

@ -3,7 +3,6 @@
(:refer-clojure :exclude [name]))
(def default-hash "0x0000000000000000000000000000000000000000000000000000000000000000")
(defn contenthash [registry ens-name cb]
(ens/resolver registry
ens-name

View File

@ -1,34 +0,0 @@
(ns status-im.utils.ethereum.stickers
(:require [status-im.utils.ethereum.core :as ethereum]
[status-im.utils.ethereum.abi-spec :as abi-spec]))
(def contracts
{:mainnet nil
:testnet "0x39d16CdB56b5a6a89e1A397A13Fe48034694316E"
:rinkeby nil})
(defn pack-count
"Returns number of packs rigestered in the contract"
[contract cb]
(ethereum/call (ethereum/call-params contract "packCount()")
(fn [count] (cb (ethereum/hex->int count)))))
(defn pack-data
"Returns vector of pack data parameters by pack id: [category owner mintable timestamp price contenthash]"
[contract pack-id cb]
(ethereum/call (ethereum/call-params contract "getPackData(uint256)" (ethereum/int->hex pack-id))
(fn [data]
(cb (abi-spec/decode data ["bytes4[]" "address" "bool" "uint256" "uint256" "bytes"])))))
(defn owned-tokens
"Returns vector of owned tokens ids in the contract by address"
[contract address cb]
(ethereum/call (ethereum/call-params contract "tokensOwnedBy(address)" (ethereum/normalized-address address))
(fn [data]
(cb (first (abi-spec/decode data ["uint256[]"]))))))
(defn token-pack-id
"Returns pack id in the contract by token id"
[contract token cb]
(ethereum/call (ethereum/call-params contract "tokenPackId(uint256)" (ethereum/int->hex token))
(fn [data] (cb (ethereum/hex->int data)))))

View File

@ -9,6 +9,7 @@
[status-im.ui.screens.wallet.utils :as wallet.utils]
[status-im.utils.config :as config]
[status-im.utils.core :as utils.core]
[status-im.utils.ethereum.abi-spec :as abi-spec]
[status-im.utils.ethereum.core :as ethereum]
[status-im.utils.ethereum.tokens :as tokens]
[status-im.utils.fx :as fx]
@ -256,9 +257,9 @@
[from {:keys [amount to gas gas-price data nonce]}]
(cond-> {:from (ethereum/normalized-address from)
:to (ethereum/normalized-address to)
:value (ethereum/int->hex amount)
:gas (ethereum/int->hex gas)
:gasPrice (ethereum/int->hex gas-price)}
:value (str "0x" (abi-spec/number-to-hex amount))
:gas (str "0x" (abi-spec/number-to-hex gas))
:gasPrice (str "0x" (abi-spec/number-to-hex gas-price))}
data
(assoc :data data)
nonce
@ -314,15 +315,15 @@
(when on-error
{:dispatch (conj on-error "transaction was cancelled by user")}))))
(defn prepare-unconfirmed-transaction [db now hash]
(defn prepare-unconfirmed-transaction
[db now hash]
(let [transaction (get-in db [:wallet :send-transaction])
all-tokens (:wallet/all-tokens db)]
(let [chain (:chain db)
token (tokens/symbol->token all-tokens (keyword chain) (:symbol transaction))]
(-> transaction
(assoc :confirmations "0"
:timestamp (str now)
:type :outbound
(assoc :timestamp (str now)
:type :pending
:hash hash
:value (:amount transaction)
:token token
@ -466,30 +467,6 @@
:wallet-send-modal-stack
:wallet-send-modal-stack-with-onboarding)]]}))
(fx/defn open-sign-transaction-flow
[{:keys [db] :as cofx}
{:keys [gas gas-price] :as transaction}]
(let [go-to-view-id (if (get-in db [:account/account :wallet-set-up-passed?])
:wallet-send-modal-stack
:wallet-send-modal-stack-with-onboarding)]
(fx/merge cofx
(cond-> {:db (-> db
(assoc-in [:navigation/screen-params :wallet-send-modal-stack :modal?] true)
(assoc-in [:wallet :send-transaction]
transaction)
(assoc-in [:wallet :send-transaction :original-gas]
gas))}
(not gas)
(assoc :wallet/update-estimated-gas
{:obj (select-keys transaction [:to :data])
:success-event :wallet/update-estimated-gas-success})
(not gas-price)
(assoc :wallet/update-gas-price
{:success-event :wallet/update-gas-price-success
:edit? false}))
(navigation/navigate-to-cofx go-to-view-id {}))))
(defn send-transaction-screen-did-load
[{:keys [db]}]
{:db (assoc-in db
@ -575,3 +552,34 @@
(toggle-visible-token symbol true)
;;TODO(goranjovic): move `update-token-balance-success` function to wallet models
(update-token-balance symbol balance)))
(fx/defn eth-transaction-call
[{:keys [db] :as cofx}
{:keys [contract method params on-success on-error details] :as transaction}]
(let [current-address (ethereum/current-address db)
transaction (merge {:to contract
:from current-address
:data (abi-spec/encode method params)
:id "approve"
:symbol :ETH
:method "eth_sendTransaction"
:amount (money/bignumber 0)
:on-success on-success
:on-error on-error}
details)
go-to-view-id (if (get-in db [:account/account :wallet-set-up-passed?])
:wallet-send-modal-stack
:wallet-send-modal-stack-with-onboarding)]
(fx/merge cofx
{:db (-> db
(assoc-in [:navigation/screen-params :wallet-send-modal-stack :modal?] true)
(assoc-in [:wallet :send-transaction]
transaction))
:wallet/update-estimated-gas
{:obj (select-keys transaction [:to :from :data])
:success-event :wallet/update-estimated-gas-success}
:wallet/update-gas-price
{:success-event :wallet/update-gas-price-success
:edit? false}}
(navigation/navigate-to-cofx go-to-view-id {}))))

View File

@ -1,136 +1,210 @@
(ns status-im.wallet.custom-tokens.core
(:require-macros [status-im.utils.views :refer [defview letsubs]])
(:require [re-frame.core :as re-frame]
(:require [clojure.string :as string]
[re-frame.core :as re-frame]
[status-im.ethereum.decode :as decode]
[status-im.ethereum.json-rpc :as json-rpc]
[status-im.i18n :as i18n]
[status-im.ui.components.colors :as colors]
[status-im.ui.components.react :as react]
[status-im.utils.ethereum.core :as ethereum]
[clojure.string :as string]
[status-im.ethereum.decode :as decode]
[status-im.utils.money :as money]
[status-im.utils.fx :as fx]
[status-im.wallet.core :as wallet]))
(re-frame/reg-fx
:wallet.custom-token/get-decimals
(fn [contract]
(ethereum/call
(ethereum/call-params contract "decimals()")
#(re-frame/dispatch [:wallet.custom-token/decimals-result %]))))
(json-rpc/eth-call
{:contract contract
:method "decimals()"
:outputs ["uint256"]
:on-success
(fn [[contract-decimals]]
(re-frame/dispatch [:wallet.custom-token/decimals-result
contract-decimals]))})))
(re-frame/reg-fx
:wallet.custom-token/get-symbol
(fn [contract]
(ethereum/call
(ethereum/call-params contract "symbol()")
#(re-frame/dispatch [:wallet.custom-token/symbol-result contract %]))))
(json-rpc/eth-call
{:contract contract
:method "symbol()"
:outputs ["string"]
:on-success
(fn [[contract-symbol]]
(re-frame/dispatch [:wallet.custom-token/symbol-result
contract
contract-symbol]))})))
(re-frame/reg-fx
:wallet.custom-token/get-balance
(fn [[contract address]]
(ethereum/call
(ethereum/call-params contract "balanceOf(address)" (ethereum/normalized-address address))
#(re-frame/dispatch [:wallet.custom-token/balance-result contract %]))))
(fn [[contract wallet-address]]
(json-rpc/eth-call
{:contract contract
:method "balanceOf(address)"
:params [wallet-address]
:outputs ["uint256"]
:on-success
(fn [[balance]]
(re-frame/dispatch [:wallet.custom-token/balance-result
contract
(money/bignumber balance)]))})))
(re-frame/reg-fx
:wallet.custom-token/get-name
(fn [contract]
(ethereum/call
(ethereum/call-params contract "name()")
#(re-frame/dispatch [:wallet.custom-token/name-result contract %]))))
(json-rpc/eth-call
{:contract contract
:method "name()"
:outputs ["string"]
:on-success
(fn [[contract-name]]
(re-frame/dispatch [:wallet.custom-token/name-result
contract
contract-name]))})))
(re-frame/reg-fx
:wallet.custom-token/get-total-supply
(fn [contract]
(ethereum/call
(ethereum/call-params contract "totalSupply()")
#(re-frame/dispatch [:wallet.custom-token/total-supply-result contract %]))))
(json-rpc/eth-call
{:contract contract
:method "totalSupply()"
:outputs ["uint256"]
:on-success
(fn [[contract-total-supply]]
(re-frame/dispatch [:wallet.custom-token/total-supply-result
contract
(money/bignumber contract-total-supply)]))})))
(re-frame/reg-fx
:wallet.custom-token/contract-address-paste
(fn []
(react/get-from-clipboard #(re-frame/dispatch [:wallet.custom-token/contract-address-is-pasted (string/trim %)]))))
(react/get-from-clipboard
#(re-frame/dispatch [:wallet.custom-token/contract-address-is-pasted
(string/trim %)]))))
(defn field-exists? [{:wallet/keys [all-tokens] :as db} field-key field-value]
(defn field-exists?
[{:wallet/keys [all-tokens] :as db} field-key field-value]
(let [chain-key (ethereum/get-chain-keyword db)]
(some #(= field-value (get % field-key)) (vals (get all-tokens chain-key)))))
(some #(= field-value (get % field-key))
(vals (get all-tokens chain-key)))))
(fx/defn total-supply-result [{:keys [db]} contract result]
(if (and (string? result) (string/starts-with? result "0x") (> (count result) 2))
{:wallet.custom-token/get-balance [contract (get-in db [:account/account :address])]}
{:db (update db :wallet/custom-token-screen merge {:in-progress? nil :error (i18n/label :t/wrong-contract)})}))
(fx/defn total-supply-result
[{:keys [db]} contract total-supply]
(if (money/valid? total-supply)
{:wallet.custom-token/get-balance
[contract (ethereum/current-address db)]}
{:db (update db
:wallet/custom-token-screen
merge {:in-progress? nil
:error (i18n/label :t/wrong-contract)})}))
(defn token-in-list? [{:wallet/keys [all-tokens] :as db} contract]
(defn token-in-list?
[{:wallet/keys [all-tokens] :as db} contract]
(let [chain-key (ethereum/get-chain-keyword db)
addresses (set (map string/lower-case (keys (get all-tokens chain-key))))]
(not (nil? (get addresses (string/lower-case contract))))))
(fx/defn contract-address-is-changed [{:keys [db]} contract]
(fx/defn contract-address-is-changed
[{:keys [db]} contract]
(if (ethereum/address? contract)
(if (token-in-list? db contract)
{:db (assoc db :wallet/custom-token-screen {:contract contract :error (i18n/label :t/already-have-asset)})}
{:db (assoc db :wallet/custom-token-screen {:contract contract :in-progress? true})
{:db (assoc db
:wallet/custom-token-screen
{:contract contract :error (i18n/label :t/already-have-asset)})}
{:db (assoc db
:wallet/custom-token-screen
{:contract contract :in-progress? true})
:wallet.custom-token/get-total-supply contract})
{:db (assoc db :wallet/custom-token-screen {:contract contract :error (i18n/label :t/wrong-address)})}))
{:db (assoc db
:wallet/custom-token-screen
{:contract contract
:error (i18n/label :t/wrong-address)})}))
(fx/defn decimals-result [{:keys [db]} result]
{:db (update db :wallet/custom-token-screen merge {:decimals (str (decode/uint result))
(fx/defn decimals-result
[{:keys [db]} result]
{:db (update db
:wallet/custom-token-screen
merge
{:decimals (str (decode/uint result))
:in-progress? nil})})
(fx/defn symbol-result [{:keys [db]} contract result]
(let [token-symbol (decode/string result)
symbol-exists? (field-exists? db :symbol (keyword token-symbol))]
(fx/defn symbol-result
[{:keys [db]} contract token-symbol]
(let [symbol-exists? (field-exists? db :symbol (keyword token-symbol))]
{:db
(update db :wallet/custom-token-screen merge
(update db
:wallet/custom-token-screen merge
{:symbol token-symbol
:error-symbol (when symbol-exists?
(i18n/label :t/you-already-have-an-asset {:value token-symbol}))})
:wallet.custom-token/get-decimals
contract}))
(fx/defn name-result [{:keys [db]} contract result]
(let [token-name (decode/string result)
name-exists? (field-exists? db :name token-name)]
(fx/defn name-result
[{:keys [db]} contract token-name]
(let [name-exists? (field-exists? db :name token-name)]
{:db
(update db :wallet/custom-token-screen merge
{:name token-name
:error-name (when name-exists?
(i18n/label :t/you-already-have-an-asset {:value token-name}))})
:wallet.custom-token/get-symbol
contract}))
(i18n/label :t/you-already-have-an-asset
{:value token-name}))})
:wallet.custom-token/get-symbol contract}))
(fx/defn balance-result [{:keys [db]} contract result]
(if (and (string? result) (string/starts-with? result "0x") (> (count result) 2))
{:db (assoc-in db [:wallet/custom-token-screen :balance] (str (decode/uint result)))
(fx/defn balance-result
[{:keys [db]} contract balance]
(if (money/valid? balance)
{:db (assoc-in db
[:wallet/custom-token-screen :balance]
(str balance))
:wallet.custom-token/get-name contract}
{:db (update db :wallet/custom-token-screen merge {:in-progress? nil :error (i18n/label :t/wrong-contract)})}))
{:db (update db
:wallet/custom-token-screen
merge
{:in-progress? nil
:error (i18n/label :t/wrong-contract)})}))
(fx/defn add-custom-token [{:keys [db] :as cofx}]
(fx/defn add-custom-token
[{:keys [db] :as cofx}]
(let [{:keys [contract name symbol decimals]} (get db :wallet/custom-token-screen)
chain-key (ethereum/get-chain-keyword db)
symbol (keyword symbol)
new-token {:address contract :name name :symbol symbol :custom? true
:decimals (int decimals) :color (rand-nth colors/chat-colors)}]
(fx/merge (assoc-in cofx [:db :wallet/all-tokens chain-key contract] new-token)
new-token {:address contract
:name name
:symbol symbol
:custom? true
:decimals (int decimals)
:color (rand-nth colors/chat-colors)}]
(fx/merge (assoc-in cofx
[:db :wallet/all-tokens chain-key contract]
new-token)
(wallet/add-custom-token new-token))))
(fx/defn remove-custom-token [{:keys [db] :as cofx} {:keys [address] :as token}]
(fx/defn remove-custom-token
[{:keys [db] :as cofx} {:keys [address] :as token}]
(let [chain-key (ethereum/get-chain-keyword db)]
(fx/merge (update-in cofx [:db :wallet/all-tokens chain-key] dissoc address)
(wallet/remove-custom-token token))))
(fx/defn field-is-edited [{:keys [db] :as cofx} field-key value]
(fx/defn field-is-edited
[{:keys [db] :as cofx} field-key value]
(case field-key
:contract (contract-address-is-changed cofx value)
:name {:db (update db :wallet/custom-token-screen merge
:name {:db (update db
:wallet/custom-token-screen merge
{field-key
value
:error-name
(when (field-exists? db field-key value)
(i18n/label :t/you-already-have-an-asset {:value value}))})}
(i18n/label :t/you-already-have-an-asset
{:value value}))})}
:symbol {:db (update db :wallet/custom-token-screen merge
{field-key
value
:error-symbol
(when (field-exists? db field-key (keyword value))
(i18n/label :t/you-already-have-an-asset {:value value}))})}
:decimals {:db (assoc-in db [:wallet/custom-token-screen :decimals] value)}))
:decimals {:db (assoc-in db
[:wallet/custom-token-screen :decimals]
value)}))

View File

@ -1,15 +1,7 @@
(ns status-im.test.utils.ethereum.core
(:require [cljs.test :refer-macros [deftest is testing]]
(:require [cljs.test :refer-macros [deftest is]]
[status-im.utils.ethereum.core :as ethereum]))
(deftest call-params
(testing "ERC20 balance-of params"
(let [contract "0x29b5f6efad2ad701952dfde9f29c960b5d6199c5"
address "0xa7cfd581060ec66414790691681732db249502bd"]
(is (= (ethereum/call-params contract "balanceOf(address)" address)
{:to "0x29b5f6efad2ad701952dfde9f29c960b5d6199c5"
:data "0x70a08231000000000000000000000000a7cfd581060ec66414790691681732db249502bd"})))))
(deftest chain-id->chain-keyword
(is (= (ethereum/chain-id->chain-keyword 1) :mainnet))
(is (= (ethereum/chain-id->chain-keyword 3) :testnet))