Browse Source

Add import feature

master
Loëck Vézien 7 years ago
parent
commit
300c866d5b
No known key found for this signature in database GPG Key ID: CBCDCE384E853AC4
  1. 1
      flow-defs/process.js
  2. 1
      package.json
  3. 31
      src/actions/accounts.js
  4. 2
      src/components/SideBar/index.js
  5. 121
      src/components/modals/AddAccount.js
  6. 24
      src/helpers/btc.js
  7. 126
      src/internals/usb/wallet/accounts.js
  8. 31
      src/internals/usb/wallet/index.js
  9. 18
      src/reducers/accounts.js
  10. 2
      src/types/common.js
  11. 58
      yarn.lock

1
flow-defs/process.js

@ -1,6 +1,7 @@
declare var process: {
send(args: any): void,
on(event: string, args: any): void,
nextTick(callback: Function): void,
title: string,
env: Object,
}

1
package.json

@ -39,7 +39,6 @@
"axios": "^0.17.1",
"bcryptjs": "^2.4.3",
"bitcoinjs-lib": "^3.3.2",
"blockchain.info": "^2.11.0",
"bs58check": "^2.1.1",
"color": "^2.0.1",
"downshift": "^1.25.0",

31
src/actions/accounts.js

@ -1,6 +1,5 @@
// @flow
import values from 'lodash/values'
import { createAction } from 'redux-actions'
import type { Dispatch } from 'redux'
@ -10,8 +9,8 @@ import db from 'helpers/db'
import type { Account } from 'types/common'
import type { State } from 'reducers'
import { getAccounts } from 'reducers/accounts'
import { getAddressData } from 'helpers/btc'
// import { getAccounts } from 'reducers/accounts'
// import { getAddressData } from 'helpers/btc'
export type AddAccount = Account => { type: string, payload: Account }
export const addAccount: AddAccount = payload => ({
@ -25,22 +24,22 @@ export const fetchAccounts: FetchAccounts = () => ({
payload: db('accounts'),
})
const setAccountData = createAction('SET_ACCOUNT_DATA', (accountID, data) => ({ accountID, data }))
// const setAccountData = createAction('SET_ACCOUNT_DATA', (accountID, data) => ({ accountID, data }))
export const syncAccount: Function = account => async (dispatch: Dispatch<*>) => {
const { address } = account
const addressData = await getAddressData(address)
dispatch(setAccountData(account.id, addressData))
// const { address } = account
// const addressData = await getAddressData(address)
// dispatch(setAccountData(account.id, addressData))
}
export const syncAccounts = () => async (dispatch: Dispatch<*>, getState: () => State) => {
const state = getState()
const accountsMap = getAccounts(state)
const accounts = values(accountsMap)
console.log(`syncing accounts...`)
await Promise.all(accounts.map(account => dispatch(syncAccount(account))))
console.log(`all accounts synced`)
// const state = getState()
// const accountsMap = getAccounts(state)
// const accounts = values(accountsMap)
//
// console.log(`syncing accounts...`)
//
// await Promise.all(accounts.map(account => dispatch(syncAccount(account))))
//
// console.log(`all accounts synced`)
}

2
src/components/SideBar/index.js

@ -79,7 +79,7 @@ class SideBar extends PureComponent<Props> {
{Object.entries(accounts).map(([id, account]: [string, any]) => (
<Item
linkTo={`/account/${id}`}
desc={`${account.type.toUpperCase()} 3.78605936`}
desc={`${account.type.toUpperCase()} ${account.data.balance}`}
key={id}
>
{account.name}

121
src/components/modals/AddAccount.js

@ -3,13 +3,15 @@
import React, { PureComponent } from 'react'
import styled from 'styled-components'
import { connect } from 'react-redux'
import { ipcRenderer } from 'electron'
import type { MapStateToProps } from 'react-redux'
import type { Device } from 'types/common'
import type { Accounts, Device } from 'types/common'
import { sendSyncEvent } from 'renderer/events'
import { getCurrentDevice } from 'reducers/devices'
import { closeModal } from 'reducers/modals'
import { getAccounts } from 'reducers/accounts'
import { getCurrentDevice } from 'reducers/devices'
import { sendEvent } from 'renderer/events'
import { addAccount } from 'actions/accounts'
@ -49,12 +51,34 @@ const Steps = {
),
connectDevice: () => <div>Connect your Ledger</div>,
startWallet: (props: Object) => <div>Select {props.wallet.toUpperCase()} App on your Ledger</div>,
confirmation: (props: Object) => (
inProgress: (props: Object) => (
<div>
Add {props.wallet.toUpperCase()} - {props.accountName} - {props.walletAddress} ?
<Button onClick={props.onConfirm}>Yes!</Button>
In progress.
{props.progress !== null && (
<div>
Account: {props.progress.account} / Transactions: {props.progress.transactions}
</div>
)}
</div>
),
listAccounts: (props: Object) => {
const accounts = Object.entries(props.accounts)
return (
<div>
{accounts.length > 0
? accounts.map(([index, account]: [string, any]) => (
<div key={index}>
<div>Balance: {account.balance}</div>
<div>Transactions: {account.transactions.length}</div>
<div>
<Button onClick={props.onAddAccount(index)}>Import</Button>
</div>
</div>
))
: 'No accounts'}
</div>
)
},
}
type InputValue = {
@ -62,20 +86,23 @@ type InputValue = {
wallet: string,
}
type Step = 'createAccount' | 'connectDevice' | 'startWallet' | 'confirmation'
type Step = 'createAccount' | 'connectDevice' | 'inProgress' | 'startWallet' | 'listAccounts'
type Props = {
addAccount: Function,
closeModal: Function,
currentDevice: Device | null,
accounts: Accounts,
}
type State = {
inputValue: InputValue,
step: Step,
walletAddress: string,
accounts: Object,
progress: null | Object,
}
const mapStateToProps: MapStateToProps<*, *, *> = state => ({
accounts: getAccounts(state),
currentDevice: getCurrentDevice(state),
})
@ -89,7 +116,8 @@ const defaultState = {
accountName: '',
wallet: '',
},
walletAddress: '',
accounts: {},
progress: null,
step: 'createAccount',
}
@ -98,6 +126,10 @@ class AddAccountModal extends PureComponent<Props, State> {
...defaultState,
}
componentDidMount() {
ipcRenderer.on('msg', this.handleWalletRequest)
}
componentWillReceiveProps(nextProps) {
const { currentDevice } = nextProps
@ -121,31 +153,21 @@ class AddAccountModal extends PureComponent<Props, State> {
getWalletInfos() {
const { inputValue } = this.state
const { currentDevice } = this.props
const { currentDevice, accounts } = this.props
if (currentDevice === null) {
return
}
const { data: { data }, type } = sendSyncEvent('usb', 'wallet.request', {
sendEvent('usb', 'wallet.request', {
path: currentDevice.path,
wallet: inputValue.wallet,
currentAccounts: Object.keys(accounts),
})
if (type === 'wallet.request.fail') {
this._timeout = setTimeout(() => this.getWalletInfos(), 1e3)
}
if (type === 'wallet.request.success') {
this.setState({
walletAddress: data.bitcoinAddress,
step: 'confirmation',
})
}
}
getStepProps() {
const { inputValue, walletAddress, step } = this.state
const { inputValue, step, progress, accounts } = this.state
const props = (predicate, props) => (predicate ? props : {})
@ -158,26 +180,57 @@ class AddAccountModal extends PureComponent<Props, State> {
...props(step === 'startWallet', {
wallet: inputValue.wallet,
}),
...props(step === 'confirmation', {
accountName: inputValue.accountName,
onConfirm: this.handleAddAccount,
wallet: inputValue.wallet,
walletAddress,
...props(step === 'inProgress', {
progress,
}),
...props(step === 'listAccounts', {
accounts,
onAddAccount: this.handleAddAccount,
}),
}
}
handleAddAccount = () => {
const { inputValue, walletAddress } = this.state
componentWillUmount() {
ipcRenderer.removeListener('msg', this.handleWalletRequest)
clearTimeout(this._timeout)
}
handleWalletRequest = (e, { data, type }) => {
if (type === 'wallet.request.progress') {
this.setState({
step: 'inProgress',
progress: data,
})
}
if (type === 'wallet.request.fail') {
this._timeout = setTimeout(() => this.getWalletInfos(), 1e3)
}
if (type === 'wallet.request.success') {
this.setState({
accounts: data,
step: 'listAccounts',
})
}
}
handleAddAccount = index => () => {
const { inputValue, accounts } = this.state
const { addAccount, closeModal } = this.props
const account = {
const { id, balance, transactions } = accounts[index]
addAccount({
id,
name: inputValue.accountName,
type: inputValue.wallet,
address: walletAddress,
}
data: {
balance,
transactions,
},
})
addAccount(account)
closeModal('add-account')
}

24
src/helpers/btc.js

@ -1,15 +1,11 @@
import blockexplorer from 'blockchain.info/blockexplorer'
const explorer = blockexplorer.usingNetwork(3)
function computeTransaction(address) {
export function computeTransaction(addresses) {
return transaction => {
const outputVal = transaction.out
.filter(o => o.addr === address)
const outputVal = transaction.outputs
.filter(o => addresses.includes(o.address))
.reduce((acc, cur) => acc + cur.value, 0)
const inputVal = transaction.inputs
.filter(i => i.prev_out.addr === address)
.reduce((acc, cur) => acc + cur.prev_out.value, 0)
.filter(i => addresses.includes(i.address))
.reduce((acc, cur) => acc + cur.value, 0)
const balance = outputVal - inputVal
return {
...transaction,
@ -17,13 +13,3 @@ function computeTransaction(address) {
}
}
}
export async function getAddressData(address) {
const addressData = await explorer.getAddress(address)
const unifiedData = {
address,
balance: addressData.final_balance,
transactions: addressData.txs.map(computeTransaction(address)),
}
return unifiedData
}

126
src/internals/usb/wallet/getAddresses.js → src/internals/usb/wallet/accounts.js

@ -5,7 +5,9 @@ import bitcoin from 'bitcoinjs-lib'
import bs58check from 'bs58check'
import Btc from '@ledgerhq/hw-app-btc'
const networks = [
import { computeTransaction } from 'helpers/btc'
export const networks = [
{
...bitcoin.networks.bitcoin,
family: 1,
@ -36,7 +38,7 @@ function parseHexString(str) {
return result
}
function createXPUB({ depth, fingerprint, childnum, chainCode, publicKey, network }) {
function createXpub({ depth, fingerprint, childnum, chainCode, publicKey, network }) {
return [
network.toString(16).padStart(8, 0),
depth.toString(16).padStart(2, 0),
@ -49,7 +51,8 @@ function createXPUB({ depth, fingerprint, childnum, chainCode, publicKey, networ
function encodeBase58Check(vchIn) {
vchIn = parseHexString(vchIn)
return bs58check.encode(new Uint8Array(vchIn))
return bs58check.encode(Buffer.from(vchIn))
}
function getPath({ coin, account, segwit }) {
@ -78,10 +81,52 @@ function getTransactions(addresses) {
)
}
export default async transport => {
const coin = 1
const account = 0
const segwit = true
export async function getAccount({ hdnode, segwit, network }) {
const script = segwit ? parseInt(network.scriptHash, 10) : parseInt(network.pubKeyHash, 10)
let transactions = []
const nextPath = start => {
const count = 20
const getAddress = path => getPublicAddress(hdnode, path, script, segwit)
return Promise.all(
Array.from(Array(count).keys()).map(v =>
Promise.all([
getAddress(`0/${v + start}`), // external chain
getAddress(`1/${v + start}`), // internal chain
]),
),
).then(async results => {
const currentAddresses = results.reduce((result, v) => [...result, ...v], [])
const { data: { txs } } = await getTransactions(currentAddresses)
transactions = [...transactions, ...txs.map(computeTransaction(currentAddresses))]
if (txs.length > 0) {
return nextPath(start + (count - 1))
}
return {
balance: transactions.reduce((result, v) => {
result += v.balance
return result
}, 0),
transactions,
}
})
}
return nextPath(0)
}
export function getHDNode({ xpub58, network }) {
return bitcoin.HDNode.fromBase58(xpub58, network)
}
export default async ({ transport, currentAccounts, onProgress, coin = 1, segwit = true }) => {
const btc = new Btc(transport)
const network = networks[coin]
@ -91,8 +136,6 @@ export default async transport => {
await transport.exchange(`e014000005${p2pkh}${p2sh}${fam.substr(-2)}`, [0x9000])
const btc = new Btc(transport)
const getPublicKey = path => btc.getWalletPublicKey(path)
let result = bitcoin.crypto.sha256(
@ -102,47 +145,54 @@ export default async transport => {
)
result = bitcoin.crypto.ripemd160(result)
const fingerprint = ((result[0] << 24) | (result[1] << 16) | (result[2] << 8) | result[3]) >>> 0
onProgress(null)
const { publicKey, chainCode } = await getPublicKey(getPath({ segwit, coin, account }))
const compressPublicKey = getCompressPublicKey(publicKey)
const fingerprint = ((result[0] << 24) | (result[1] << 16) | (result[2] << 8) | result[3]) >>> 0
const childnum = (0x80000000 | account) >>> 0
const xpub = createXPUB({
depth: 3,
fingerprint,
childnum,
chainCode,
publicKey: compressPublicKey,
network: network.bip32.public,
})
const getXpub58ByAccount = async ({ account, network }) => {
const { publicKey, chainCode } = await getPublicKey(getPath({ segwit, coin, account }))
const compressPublicKey = getCompressPublicKey(publicKey)
const xpub58 = encodeBase58Check(xpub)
const childnum = (0x80000000 | account) >>> 0
const hdnode = bitcoin.HDNode.fromBase58(xpub58, network)
const xpub = createXpub({
depth: 3,
fingerprint,
childnum,
chainCode,
publicKey: compressPublicKey,
network: network.bip32.public,
})
const script = segwit ? parseInt(network.scriptHash, 10) : parseInt(network.pubKeyHash, 10)
return encodeBase58Check(xpub)
}
const nextPath = async i => {
if (i <= 0x7fffffff) {
for (let j = 0; j < 2; j++) {
const path = `${j}/${i}`
const getAllAccounts = async (currentAccount = 0, accounts = {}) => {
const xpub58 = await getXpub58ByAccount({ account: currentAccount, network })
const address = getPublicAddress(hdnode, path, script, segwit)
console.log('address', address)
if (currentAccounts.includes(xpub58)) {
return getAllAccounts(currentAccount + 1, accounts) // skip existing account
}
const { data: { txs } } = await getTransactions(address) // eslint-disable-line no-await-in-loop
const hdnode = getHDNode({ xpub58, network })
const { transactions, balance } = await getAccount({ hdnode, network, segwit })
console.log('txs', txs.length)
onProgress({
account: currentAccount,
transactions: transactions.length,
})
if (j === 1 && i < 10) {
nextPath(++i)
}
if (transactions.length > 0) {
accounts[currentAccount] = {
id: xpub58,
balance,
transactions,
}
} else {
console.log('meeeh')
return getAllAccounts(currentAccount + 1, accounts)
}
return accounts
}
nextPath(0)
return getAllAccounts()
}

31
src/internals/usb/wallet/index.js

@ -1,24 +1,39 @@
// @flow
import CommNodeHid from '@ledgerhq/hw-transport-node-hid'
import getAddresses from './getAddresses'
async function getWallet(path, wallet) {
import getAllAccounts from './accounts'
async function getAllAccountsByWallet({ path, wallet, currentAccounts, onProgress }) {
const transport = await CommNodeHid.open(path)
console.log('getWallet', path)
if (wallet === 'btc') {
await getAddresses(transport)
return getAllAccounts({ transport, currentAccounts, onProgress })
}
throw new Error('invalid wallet')
}
export default (sendEvent: Function) => ({
request: async ({ path, wallet }: { path: string, wallet: string }) => {
request: async ({
path,
wallet,
currentAccounts,
}: {
path: string,
wallet: string,
currentAccounts: Array<*>,
}) => {
try {
const data = await getWallet(path, wallet)
sendEvent('wallet.request.success', { path, wallet, data })
const data = await getAllAccountsByWallet({
path,
wallet,
currentAccounts,
onProgress: progress => sendEvent('wallet.request.progress', progress, { kill: false }),
})
sendEvent('wallet.request.success', data)
} catch (err) {
sendEvent('wallet.request.fail', { path, wallet, err: err.stack || err })
sendEvent('wallet.request.fail', err.stack || err)
}
},
})

18
src/reducers/accounts.js

@ -1,7 +1,6 @@
// @flow
import { handleActions } from 'redux-actions'
import shortid from 'shortid'
import get from 'lodash/get'
import type { State } from 'reducers'
@ -12,17 +11,12 @@ export type AccountsState = Accounts
const state: AccountsState = {}
const handlers: Object = {
ADD_ACCOUNT: (state: AccountsState, { payload: account }: { payload: Account }) => {
const id = shortid.generate()
return {
...state,
[id]: {
id,
...account,
},
}
},
ADD_ACCOUNT: (state: AccountsState, { payload: account }: { payload: Account }) => ({
...state,
[account.id]: {
...account,
},
}),
FETCH_ACCOUNTS: (state: AccountsState, { payload: accounts }: { payload: Accounts }) => accounts,
SET_ACCOUNT_DATA: (
state: AccountsState,

2
src/types/common.js

@ -18,7 +18,6 @@ export type Transaction = {
// -------------------- Accounts
export type AccountData = {
address: string,
balance: number,
transactions: Array<Transaction>,
}
@ -27,7 +26,6 @@ export type Account = {
id: string,
name: string,
type: string,
address: string,
data?: AccountData,
}

58
yarn.lock

@ -1787,28 +1787,12 @@ block-stream@*:
dependencies:
inherits "~2.0.0"
blockchain.info@^2.11.0:
version "2.11.0"
resolved "https://registry.yarnpkg.com/blockchain.info/-/blockchain.info-2.11.0.tgz#63b46617e194164d377e183e6c667d3ef38ad5b6"
dependencies:
q "^1.4.1"
request-promise "^0.4.3"
url-join "0.0.1"
url-parse "^1.0.5"
url-pattern "^0.10.2"
optionalDependencies:
ws "^1.1.2"
bluebird-lst@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/bluebird-lst/-/bluebird-lst-1.0.5.tgz#bebc83026b7e92a72871a3dc599e219cbfb002a9"
dependencies:
bluebird "^3.5.1"
bluebird@^2.3:
version "2.11.0"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1"
bluebird@^3.4.7, bluebird@^3.5.0, bluebird@^3.5.1:
version "3.5.1"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
@ -2195,7 +2179,7 @@ chalk@0.5.1:
strip-ansi "^0.3.0"
supports-color "^0.2.0"
chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
dependencies:
@ -5681,7 +5665,7 @@ lodash.uniq@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
lodash@^3.10.0, lodash@^3.10.1:
lodash@^3.10.1:
version "3.10.1"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
@ -6378,10 +6362,6 @@ optionator@^0.8.2:
type-check "~0.3.2"
wordwrap "~1.0.0"
options@>=0.0.5:
version "0.0.6"
resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f"
ora@^0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4"
@ -7151,7 +7131,7 @@ pushdata-bitcoin@^1.0.1:
dependencies:
bitcoin-ops "^1.3.0"
q@^1.1.2, q@^1.4.1:
q@^1.1.2:
version "1.5.1"
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
@ -7772,15 +7752,6 @@ repeating@^2.0.0:
dependencies:
is-finite "^1.0.0"
request-promise@^0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/request-promise/-/request-promise-0.4.3.tgz#3c8ddc82f06f8908d720aede1d6794258e22121c"
dependencies:
bluebird "^2.3"
chalk "^1.1.0"
lodash "^3.10.0"
request "^2.34"
request@2.81.0:
version "2.81.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
@ -7808,7 +7779,7 @@ request@2.81.0:
tunnel-agent "^0.6.0"
uuid "^3.0.0"
request@^2.34, request@^2.45.0, request@^2.81.0, request@^2.83.0:
request@^2.45.0, request@^2.81.0, request@^2.83.0:
version "2.83.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356"
dependencies:
@ -8928,10 +8899,6 @@ uid-number@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
ultron@1.0.x:
version "1.0.2"
resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa"
union-value@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4"
@ -9029,10 +8996,6 @@ urix@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
url-join@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/url-join/-/url-join-0.0.1.tgz#1db48ad422d3402469a87f7d97bdebfe4fb1e3c8"
url-loader@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-0.6.2.tgz#a007a7109620e9d988d14bce677a1decb9a993f7"
@ -9054,17 +9017,13 @@ url-parse@1.0.x:
querystringify "0.0.x"
requires-port "1.0.x"
url-parse@^1.0.5, url-parse@^1.1.8:
url-parse@^1.1.8:
version "1.2.0"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.2.0.tgz#3a19e8aaa6d023ddd27dcc44cb4fc8f7fec23986"
dependencies:
querystringify "~1.0.0"
requires-port "~1.0.0"
url-pattern@^0.10.2:
version "0.10.2"
resolved "https://registry.yarnpkg.com/url-pattern/-/url-pattern-0.10.2.tgz#e9f07104982b72312db4473dd86a527b580015da"
url-to-options@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9"
@ -9384,13 +9343,6 @@ write@^0.2.1:
dependencies:
mkdirp "^0.5.1"
ws@^1.1.2:
version "1.1.5"
resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.5.tgz#cbd9e6e75e09fc5d2c90015f21f0c40875e0dd51"
dependencies:
options ">=0.0.5"
ultron "1.0.x"
xdg-basedir@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"

Loading…
Cancel
Save