add ibc transfer

This commit is contained in:
liangping 2021-09-17 22:09:39 +08:00
parent e85701f58c
commit 9fd2aa33ed
8 changed files with 567 additions and 10 deletions

View File

@ -222,6 +222,11 @@ const chainAPI = class ChainFetch {
return ret return ret
} }
async getUrl(url) {
this.getSelectedConfig()
return fetch(url).then(res => res.json())
}
static fetch(host, url) { static fetch(host, url) {
const ret = fetch(host + url).then(response => response.json()) const ret = fetch(host + url).then(response => response.json())
return ret return ret
@ -267,13 +272,18 @@ const chainAPI = class ChainFetch {
return this.get('/ibc/applications/transfer/v1beta1/denom_traces/'.concat(h), config).then(data => commonProcess(data)) return this.get('/ibc/applications/transfer/v1beta1/denom_traces/'.concat(h), config).then(data => commonProcess(data))
} }
async getIBCChannels(key = null, config = null) { async getIBCChannels(config = null, key = null) {
if (key) { if (key) {
return this.get('/ibc/core/channel/v1beta1/channels?pagination.key='.concat(key), config).then(data => commonProcess(data)) return this.get('/ibc/core/channel/v1beta1/channels?pagination.key='.concat(key), config).then(data => commonProcess(data))
} }
return this.get('/ibc/core/channel/v1beta1/channels', config).then(data => commonProcess(data)) return this.get('/ibc/core/channel/v1beta1/channels', config).then(data => commonProcess(data))
} }
// /cosmos/staking/v1beta1/delegations/{delegator_addr}
// eslint-disable-next-line camelcase
async getIBCChannelClientState(channel_id, port_id, config = null) {
// eslint-disable-next-line camelcase
return this.get(`/ibc/core/channel/v1beta1/channels/${channel_id}/ports/${port_id}/client_state`, config).then(data => commonProcess(data))
}
static async getBankBalance(baseurl, address) { static async getBankBalance(baseurl, address) {
return ChainFetch.fetch(baseurl, '/bank/balances/'.concat(address)).then(data => commonProcess(data)) return ChainFetch.fetch(baseurl, '/bank/balances/'.concat(address)).then(data => commonProcess(data))

View File

@ -47,6 +47,7 @@ export default {
selected, selected,
avatars: {}, avatars: {},
height: 0, height: 0,
ibcChannels: {},
}, },
getters: { getters: {
getchains: state => state.chains, getchains: state => state.chains,
@ -65,6 +66,9 @@ export default {
setHeight(state, height) { setHeight(state, height) {
state.chains.height = height state.chains.height = height
}, },
setChannels(state, { chain, channels }) {
state.chains.ibcChannels[chain] = channels
},
}, },
actions: {}, actions: {},
} }

View File

@ -208,7 +208,7 @@
<b-card-body> <b-card-body>
<b-table <b-table
stacked="sm" stacked="sm"
:items="deposits" :items="deposits.deposits?deposits.deposits:deposits"
:fields="deposit_fields" :fields="deposit_fields"
striped striped
> >

View File

@ -0,0 +1,530 @@
<template>
<div>
<b-modal
id="ibc-transfer-window"
centered
size="md"
title="IBC Transfer Tokens"
ok-title="Send"
hide-header-close
scrollable
:ok-disabled="!address"
@hidden="resetModal"
@ok="handleOk"
@show="loadBalance"
>
<validation-observer ref="simpleRules">
<b-form>
<b-row>
<b-col v-if="account">
<b-form-group
label="Sender"
label-for="Account"
>
<b-input-group class="mb-25">
<b-input-group-prepend is-text>
<b-avatar
:src="account.logo"
size="18"
variant="light-primary"
rounded
/>
</b-input-group-prepend>
<b-form-input
:value="account.addr"
readonly
/>
</b-input-group>
</b-form-group>
</b-col>
</b-row>
<b-row>
<b-col>
<b-form-group
label="Available Token"
label-for="Token"
>
<validation-provider
#default="{ errors }"
rules="required"
name="Token"
>
<b-form-select
v-model="token"
>
<b-form-select-option
v-for="item in balance"
:key="item.denom"
:value="item.denom"
>
{{ format(item) }}
</b-form-select-option>
</b-form-select>
<small class="text-danger">{{ errors[0] }}</small>
</validation-provider>
</b-form-group>
</b-col>
</b-row>
<b-row>
<b-col>
<b-form-group
label="Amount"
label-for="Amount"
>
<validation-provider
v-slot="{ errors }"
rules="required|regex:^([0-9\.]+)$"
name="amount"
>
<b-input-group class="mb-25">
<b-form-input
id="Amount"
v-model="amount"
:state="errors.length > 0 ? false:null"
placeholder="Input a number"
type="number"
/>
<b-input-group-append is-text>
{{ printDenom() }}
</b-input-group-append>
</b-input-group>
<small class="text-danger">{{ errors[0] }}</small>
</validation-provider>
</b-form-group>
</b-col>
</b-row>
<b-row>
<b-col>
<b-form-group
label="Destination"
label-for="destination"
>
<validation-provider
#default="{ errors }"
rules="required"
name="destination"
>
<b-input-group class="mb-25">
<v-select
v-model="destination"
placeholder="Select a channel"
/>
</b-input-group>
<small class="text-danger">{{ errors[0] }}</small>
</validation-provider>
</b-form-group>
</b-col>
</b-row>
<b-row>
<b-col>
<b-form-group
label="Recipient"
label-for="Recipient"
>
<validation-provider
#default="{ errors }"
rules="required"
name="recipient"
>
<b-input-group class="mb-25">
<b-form-input
id="Recipient"
v-model="recipient"
:state="errors.length > 0 ? false:null"
/>
</b-input-group>
<small class="text-danger">{{ errors[0] }}</small>
</validation-provider>
</b-form-group>
</b-col>
</b-row>
<b-row>
<b-col>
<b-form-group
label="Fee"
label-for="Fee"
>
<validation-provider
v-slot="{ errors }"
rules="required|integer"
name="fee"
>
<b-input-group>
<b-form-input v-model="fee" />
<b-input-group-append>
<b-form-select
v-model="feeDenom"
:options="feeDenoms"
value-field="denom"
text-field="denom"
/>
</b-input-group-append>
</b-input-group>
<small class="text-danger">{{ errors[0] }}</small>
</validation-provider>
</b-form-group>
</b-col>
<b-col cols="12">
<b-form-group>
<b-form-checkbox
v-model="advance"
name="advance"
value="true"
>
<small>Advance</small>
</b-form-checkbox>
</b-form-group>
</b-col>
</b-row>
<b-row v-if="advance">
<b-col cols="12">
<b-form-group
label="Gas"
label-for="gas"
>
<validation-provider
v-slot="{ errors }"
name="gas"
>
<b-form-input
id="gas"
v-model="gas"
type="number"
/>
<small class="text-danger">{{ errors[0] }}</small>
</validation-provider>
</b-form-group>
</b-col>
<b-col cols="12">
<b-form-group
label="Memo"
label-for="Memo"
>
<validation-provider
v-slot="{ errors }"
name="memo"
>
<b-form-input
id="Memo"
v-model="memo"
max="2"
/>
<small class="text-danger">{{ errors[0] }}</small>
</validation-provider>
</b-form-group>
</b-col>
</b-row>
<b-row>
<b-col>
<b-form-group
label="Wallet"
label-for="wallet"
>
<validation-provider
v-slot="{ errors }"
rules="required"
name="wallet"
>
<b-form-radio-group
v-model="wallet"
stacked
class="demo-inline-spacing"
>
<b-form-radio
v-model="wallet"
name="wallet"
value="keplr"
class="d-none d-md-block"
>
Keplr
</b-form-radio>
<b-form-radio
v-model="wallet"
name="wallet"
value="ledgerUSB"
>
<small>Ledger(USB)</small>
</b-form-radio>
<b-form-radio
v-model="wallet"
name="wallet"
value="ledgerBle"
class="mr-0"
>
<small>Ledger(Bluetooth)</small>
</b-form-radio>
</b-form-radio-group>
<small class="text-danger">{{ errors[0] }}</small>
</validation-provider>
</b-form-group>
</b-col>
</b-row>
</b-form>
</validation-observer>
{{ error }}
</b-modal>
</div>
</template>
<script>
import { ValidationProvider, ValidationObserver } from 'vee-validate'
import {
BModal, BRow, BCol, BInputGroup, BInputGroupAppend, BFormInput, BAvatar, BFormGroup, BFormSelect, BFormSelectOption,
BForm, BFormRadioGroup, BFormRadio, BInputGroupPrepend, BFormCheckbox,
} from 'bootstrap-vue'
import {
required, email, url, between, alpha, integer, password, min, digits, alphaDash, length,
} from '@validations'
import {
formatToken, formatTokenDenom, getLocalAccounts, getLocalChains, setLocalTxHistory, sign, timeIn,
} from '@/libs/data'
import { Cosmos } from '@cosmostation/cosmosjs'
import vSelect from 'vue-select'
import ToastificationContent from '@core/components/toastification/ToastificationContent.vue'
export default {
name: 'TransforDialogue',
components: {
BModal,
BRow,
BCol,
BForm,
BInputGroup,
BInputGroupAppend,
BInputGroupPrepend,
BFormInput,
BAvatar,
BFormGroup,
BFormSelect,
BFormSelectOption,
BFormRadioGroup,
BFormRadio,
BFormCheckbox,
vSelect,
ValidationProvider,
ValidationObserver,
// eslint-disable-next-line vue/no-unused-components
ToastificationContent,
},
props: {
address: {
type: String,
default: '',
},
},
data() {
return {
chainId: '',
selectedChain: '',
balance: [],
token: '',
amount: null,
memo: '',
recipient: null,
fee: '800',
feeDenom: '',
wallet: 'ledgerUSB',
error: null,
sequence: 1,
accountNumber: 0,
account: [],
IBCDenom: {},
gas: '200000',
advance: false,
paths: {},
destination: {},
required,
password,
email,
min,
integer,
url,
alpha,
between,
digits,
length,
alphaDash,
}
},
computed: {
feeDenoms() {
return this.balance.filter(item => !item.denom.startsWith('ibc'))
},
},
created() {
// console.log('address: ', this.address)
},
methods: {
printDenom() {
return formatTokenDenom(this.IBCDenom[this.token] || this.token)
},
computeAccount() {
const accounts = getLocalAccounts()
const chains = getLocalChains()
const values = Object.values(accounts)
for (let i = 0; i < values.length; i += 1) {
const addr = values[i].address.find(x => x.addr === this.address)
if (addr) {
this.selectedChain = chains[addr.chain]
return addr
}
}
return null
},
loadBalance() {
this.account = this.computeAccount()
if (this.account && this.account.length > 0) this.address = this.account[0].addr
if (this.address) {
this.$http.getBankBalances(this.address, this.selectedChain).then(res => {
if (res && res.length > 0) {
this.balance = res
this.token = this.balance[0].denom
this.feeDenom = this.balance.find(x => !x.denom.startsWith('ibc')).denom
this.balance.filter(i => i.denom.startsWith('ibc')).forEach(x => {
if (!this.IBCDenom[x.denom]) {
this.$http.getIBCDenomTrace(x.denom, this.selectedChain).then(denom => {
this.IBCDenom[x.denom] = denom.denom_trace.base_denom
// console.log(denom.denom_trace)
const path = denom.denom_trace.path.split('/')
this.paths[x.denom] = {
channel: path[1],
port: path[0],
}
})
}
})
}
})
this.$http.getLatestBlock(this.selectedChain).then(ret => {
this.chainId = ret.block.header.chain_id
const notSynced = timeIn(ret.block.header.time, 10, 'm')
if (notSynced) {
this.error = 'Client is not synced or blockchain is halted'
} else {
this.error = null
}
})
this.$http.getAuthAccount(this.address, this.selectedChain).then(ret => {
if (ret.value.base_vesting_account) {
this.accountNumber = ret.value.base_vesting_account.base_account.account_number
this.sequence = ret.value.base_vesting_account.base_account.sequence
if (!this.sequence) this.sequence = 0
} else {
this.accountNumber = ret.value.account_number
this.sequence = ret.value.sequence ? ret.value.sequence : 0
}
})
const channels = this.$store.state.chains.ibcChannels[this.selectedChain.chain_name]
if (!channels) {
this.$http.getIBCChannels(this.selectedChain, null).then(ret => {
const chans = ret.channels.filter(x => x.state === 'STATE_OPEN').map(x => ({ channel_id: x.channel_id, port_id: x.port_id }))
chans.forEach((x, i) => {
this.$http.getIBCChannelClientState(x.channel_id, x.port_id, this.selectedChain).then(cs => {
chans[i].chain_id = cs.identified_client_state.client_state.chain_id
// console.log(i, chans[i])
})
})
})
}
}
},
handleOk(bvModalEvt) {
// console.log('send')
// Prevent modal from closing
bvModalEvt.preventDefault()
// Trigger submit handler
// this.handleSubmit()
this.send().then(ret => {
// console.log(ret)
this.error = ret
})
},
resetModal() {
this.feeDenom = ''
this.error = null
},
format(v) {
return formatToken(v, this.IBCDenom)
},
async sendCosmos() {
const cosmos = new Cosmos(this.selectedChain.api, this.chainId)
cosmos.getAccounts()
},
async send() {
const txMsgs = [
{
typeUrl: '/ibc.applications.transfer.v1.MsgTransfer',
value: {
source_port: this.paths[this.token].port,
source_channel: this.paths[this.token].channel,
token: {
amount: String((Number(this.amount) * 1000000).toFixed()),
denom: this.token,
},
sender: this.address,
receiver: this.recipient,
// timeout_height: this.timeoutHeight ? { revision_height: '', revision_number: '' } : {},
// timeout_timestamp: null,
},
},
]
// console.log(txMsgs)
if (this.token) return
const txFee = {
amount: [
{
amount: this.fee,
denom: this.feeDenom,
},
],
gas: this.gas,
}
const signerData = {
accountNumber: this.accountNumber,
sequence: this.sequence,
chainId: this.chainId,
}
sign(
this.wallet,
this.chainId,
this.address,
txMsgs,
txFee,
this.memo,
signerData,
).then(bodyBytes => {
this.$http.broadcastTx(bodyBytes, this.selectedChain).then(res => {
setLocalTxHistory({ op: 'send', hash: res.txhash, time: new Date() })
this.$bvModal.hide('ibc-transfer-window')
this.$toast({
component: ToastificationContent,
props: {
title: 'Transaction sent!',
icon: 'EditIcon',
variant: 'success',
},
})
}).catch(e => {
this.error = e
})
}).catch(e => {
this.error = e
})
// Send tokens
// return client.sendTokens(this.address, this.recipient, sendCoins, this.memo)
// return
},
},
}
</script>
<style lang="scss">
@import '@core/scss/vue/libs/vue-select.scss';
</style>

View File

@ -359,7 +359,7 @@ export default {
this.feeDenom = this.balance.find(x => !x.denom.startsWith('ibc')).denom this.feeDenom = this.balance.find(x => !x.denom.startsWith('ibc')).denom
this.balance.filter(i => i.denom.startsWith('ibc')).forEach(x => { this.balance.filter(i => i.denom.startsWith('ibc')).forEach(x => {
if (!this.IBCDenom[x.denom]) { if (!this.IBCDenom[x.denom]) {
this.$http.getIBCDenomTrace(x.denom).then(denom => { this.$http.getIBCDenomTrace(x.denom, this.selectedChain).then(denom => {
this.IBCDenom[x.denom] = denom.denom_trace.base_denom this.IBCDenom[x.denom] = denom.denom_trace.base_denom
}) })
} }

View File

@ -178,7 +178,6 @@ import {
import { import {
formatToken, getLocalAccounts, getLocalChains, sign, timeIn, setLocalTxHistory, formatToken, getLocalAccounts, getLocalChains, sign, timeIn, setLocalTxHistory,
} from '@/libs/data' } from '@/libs/data'
import chainAPI from '@/libs/fetch'
import ToastificationContent from '@core/components/toastification/ToastificationContent.vue' import ToastificationContent from '@core/components/toastification/ToastificationContent.vue'
export default { export default {
@ -266,14 +265,14 @@ export default {
this.account = this.computeAccount() this.account = this.computeAccount()
if (this.account && this.account.length > 0) this.address = this.account[0].addr if (this.account && this.account.length > 0) this.address = this.account[0].addr
if (this.address) { if (this.address) {
chainAPI.getBankBalance(this.selectedChain.api, this.address).then(res => { this.$http.getBankBalances(this.address).then(res => {
if (res && res.length > 0) { if (res && res.length > 0) {
this.balance = res this.balance = res
const token = this.balance.find(i => !i.denom.startsWith('ibc')) const token = this.balance.find(i => !i.denom.startsWith('ibc'))
if (token) this.feeDenom = token.denom if (token) this.feeDenom = token.denom
} }
}) })
this.$http.getLatestBlock(this.selectedChain).then(ret => { this.$http.getLatestBlock().then(ret => {
this.chainId = ret.block.header.chain_id this.chainId = ret.block.header.chain_id
const notSynced = timeIn(ret.block.header.time, 10, 'm') const notSynced = timeIn(ret.block.header.time, 10, 'm')
if (notSynced) { if (notSynced) {
@ -282,7 +281,7 @@ export default {
this.error = null this.error = null
} }
}) })
this.$http.getAuthAccount(this.address, this.selectedChain).then(ret => { this.$http.getAuthAccount(this.address).then(ret => {
if (ret.value.base_vesting_account) { if (ret.value.base_vesting_account) {
this.accountNumber = ret.value.base_vesting_account.base_account.account_number this.accountNumber = ret.value.base_vesting_account.base_account.account_number
this.sequence = ret.value.base_vesting_account.base_account.sequence this.sequence = ret.value.base_vesting_account.base_account.sequence

View File

@ -256,6 +256,10 @@
<b-td> End Time </b-td><b-td> {{ formatTime(account.value.base_vesting_account.end_time) }}</b-td> <b-td> End Time </b-td><b-td> {{ formatTime(account.value.base_vesting_account.end_time) }}</b-td>
</b-tr> </b-tr>
</b-tbody> </b-tbody>
<object-field-component
v-else
:tablefield="account.value?account.value:account"
/>
</b-table-simple> </b-table-simple>
</b-card> </b-card>
@ -392,10 +396,10 @@ export default {
}, },
computed: { computed: {
accountTitle() { accountTitle() {
if (this.account) { if (this.account && this.account.type) {
return this.account.type.substring(this.account.type.indexOf('/') + 1) return this.account.type.substring(this.account.type.indexOf('/') + 1)
} }
return '' return 'Profile'
}, },
assetTable() { assetTable() {
let total = [] let total = []

View File

@ -52,6 +52,13 @@
> >
<feather-icon icon="SendIcon" /> Transfer <feather-icon icon="SendIcon" /> Transfer
</b-dropdown-item> </b-dropdown-item>
<b-dropdown-item
v-if="balances[acc.addr]"
v-b-modal.ibc-transfer-window
@click="transfer(acc.addr)"
>
<feather-icon icon="SendIcon" /> IBC Transfer
</b-dropdown-item>
<b-dropdown-item <b-dropdown-item
v-if="balances[acc.addr]" v-if="balances[acc.addr]"
:to="`/${acc.chain}/account/${acc.addr}`" :to="`/${acc.chain}/account/${acc.addr}`"
@ -124,6 +131,7 @@
<operation-transfer-component <operation-transfer-component
:address.sync="selectedAddress" :address.sync="selectedAddress"
/> />
<operation-transfer-2-component :address="selectedAddress" />
</div> </div>
</template> </template>
@ -139,6 +147,7 @@ import {
} from '@/libs/data' } from '@/libs/data'
import ToastificationContent from '@core/components/toastification/ToastificationContent.vue' import ToastificationContent from '@core/components/toastification/ToastificationContent.vue'
import OperationTransferComponent from './OperationTransferComponent.vue' import OperationTransferComponent from './OperationTransferComponent.vue'
import OperationTransfer2Component from './OperationTransfer2Component.vue'
export default { export default {
components: { components: {
@ -157,6 +166,7 @@ export default {
OperationTransferComponent, OperationTransferComponent,
// eslint-disable-next-line vue/no-unused-components // eslint-disable-next-line vue/no-unused-components
ToastificationContent, ToastificationContent,
OperationTransfer2Component,
}, },
directives: { directives: {
'b-modal': VBModal, 'b-modal': VBModal,