x/ibc-transfer: ADR001 source tracing implementation (#6871)
* x/ibc-transfer: ADR001 source tracing implementation * gRPC proto file * validation * fix validation * import export genesis * relay.go updates * gRPC service methods * client CLI * update implementation * build * trace test * fix CLI tx args * genesis import/export tests * update comments * update proto files * GRPC tests * remove field from packet * fix coin validation bug * more validations * update comments * minor refactor * update relay.go * try fix test * minor updates * fix tests * fix test * ADR updates and comments * build * Apply suggestions from code review Co-authored-by: Aditya <adityasripal@gmail.com> Co-authored-by: colin axnér <25233464+colin-axner@users.noreply.github.com> * address a few comments from review * gRPC annotations * update proto files * Apply suggestions from code review Co-authored-by: colin axnér <25233464+colin-axner@users.noreply.github.com> * address comments * docs and changelog * sort traces * final changes to ADR * client support for full path denom prefixes * address @AdityaSripal comments * address TODO * increase test timeouts Co-authored-by: Aditya <adityasripal@gmail.com> Co-authored-by: colin axnér <25233464+colin-axner@users.noreply.github.com>
This commit is contained in:
parent
8de96d16f9
commit
3022fe9044
16
.github/workflows/test.yml
vendored
16
.github/workflows/test.yml
vendored
@ -44,7 +44,7 @@ jobs:
|
||||
test-coverage-run-1:
|
||||
runs-on: ubuntu-latest
|
||||
needs: split-test-files
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: technote-space/get-diff-action@v3
|
||||
@ -60,7 +60,7 @@ jobs:
|
||||
if: "env.GIT_DIFF != ''"
|
||||
- name: test & coverage report creation
|
||||
run: |
|
||||
cat xaa.txt | xargs go test -mod=readonly -timeout 8m -coverprofile=coverage.txt -covermode=atomic -tags='ledger test_ledger_mock'
|
||||
cat xaa.txt | xargs go test -mod=readonly -timeout 15m -coverprofile=coverage.txt -covermode=atomic -tags='ledger test_ledger_mock'
|
||||
if: "env.GIT_DIFF != ''"
|
||||
- name: filter out DONTCOVER
|
||||
run: |
|
||||
@ -81,7 +81,7 @@ jobs:
|
||||
test-coverage-run-2:
|
||||
runs-on: ubuntu-latest
|
||||
needs: split-test-files
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: technote-space/get-diff-action@v3
|
||||
@ -97,7 +97,7 @@ jobs:
|
||||
if: "env.GIT_DIFF != ''"
|
||||
- name: test & coverage report creation
|
||||
run: |
|
||||
cat xab.txt | xargs go test -mod=readonly -timeout 6m -coverprofile=coverage.txt -covermode=atomic -tags='ledger test_ledger_mock'
|
||||
cat xab.txt | xargs go test -mod=readonly -timeout 15m -coverprofile=coverage.txt -covermode=atomic -tags='ledger test_ledger_mock'
|
||||
if: "env.GIT_DIFF != ''"
|
||||
- name: filter out DONTCOVER
|
||||
run: |
|
||||
@ -118,7 +118,7 @@ jobs:
|
||||
test-coverage-run-3:
|
||||
runs-on: ubuntu-latest
|
||||
needs: split-test-files
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: technote-space/get-diff-action@v3
|
||||
@ -134,7 +134,7 @@ jobs:
|
||||
if: "env.GIT_DIFF != ''"
|
||||
- name: test & coverage report creation
|
||||
run: |
|
||||
cat xac.txt | xargs go test -mod=readonly -timeout 6m -coverprofile=coverage.txt -covermode=atomic -tags='ledger test_ledger_mock'
|
||||
cat xac.txt | xargs go test -mod=readonly -timeout 15m -coverprofile=coverage.txt -covermode=atomic -tags='ledger test_ledger_mock'
|
||||
if: "env.GIT_DIFF != ''"
|
||||
- name: filter out DONTCOVER
|
||||
run: |
|
||||
@ -155,7 +155,7 @@ jobs:
|
||||
test-coverage-run-4:
|
||||
runs-on: ubuntu-latest
|
||||
needs: split-test-files
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: technote-space/get-diff-action@v3
|
||||
@ -171,7 +171,7 @@ jobs:
|
||||
if: "env.GIT_DIFF != ''"
|
||||
- name: test & coverage report creation
|
||||
run: |
|
||||
cat xad.txt | xargs go test -mod=readonly -timeout 6m -coverprofile=coverage.txt -covermode=atomic -tags='ledger test_ledger_mock'
|
||||
cat xad.txt | xargs go test -mod=readonly -timeout 15m -coverprofile=coverage.txt -covermode=atomic -tags='ledger test_ledger_mock'
|
||||
if: "env.GIT_DIFF != ''"
|
||||
- name: filter out DONTCOVER
|
||||
run: |
|
||||
|
||||
@ -283,9 +283,10 @@ Buffers for state serialization instead of Amino.
|
||||
|
||||
### Improvements
|
||||
|
||||
* (x/ibc-transfer) [\#6871](https://github.com/cosmos/cosmos-sdk/pull/6871) Implement [ADR 001 - Coin Source Tracing](./docs/architecture/adr-001-coin-source-tracing.md).
|
||||
* (types) [\#7027](https://github.com/cosmos/cosmos-sdk/pull/7027) `Coin(s)` and `DecCoin(s)` updates:
|
||||
* Bump denomination max length to 128
|
||||
* Allow unicode letters and numbers in denominations
|
||||
* Allow uppercase letters and numbers in denominations to support [ADR 001](./docs/architecture/adr-001-coin-source-tracing.md)
|
||||
* Added `Validate` function that returns a descriptive error
|
||||
* (baseapp) [\#6186](https://github.com/cosmos/cosmos-sdk/issues/6186) Support emitting events during `AnteHandler` execution.
|
||||
* (x/auth) [\#5702](https://github.com/cosmos/cosmos-sdk/pull/5702) Add parameter querying support for `x/auth`.
|
||||
|
||||
@ -3,10 +3,11 @@
|
||||
## Changelog
|
||||
|
||||
- 2020-07-09: Initial Draft
|
||||
- 2020-08-11: Implementation changes
|
||||
|
||||
## Status
|
||||
|
||||
Proposed
|
||||
Accepted, Implemented
|
||||
|
||||
## Context
|
||||
|
||||
@ -114,7 +115,7 @@ trace the token back to the originating chain, as specified on ICS20.
|
||||
The new proposed format will be the following:
|
||||
|
||||
```golang
|
||||
ibcDenom = "ibc/" + hash(trace + "/" + base denom)
|
||||
ibcDenom = "ibc/" + hash(trace path + "/" + base denom)
|
||||
```
|
||||
|
||||
The hash function will be a SHA256 hash of the fields of the `DenomTrace`:
|
||||
@ -124,7 +125,7 @@ The hash function will be a SHA256 hash of the fields of the `DenomTrace`:
|
||||
// information
|
||||
message DenomTrace {
|
||||
// chain of port/channel identifiers used for tracing the source of the fungible token
|
||||
string trace = 1;
|
||||
string path = 1;
|
||||
// base denomination of the relayed fungible token
|
||||
string base_denom = 2;
|
||||
}
|
||||
@ -133,50 +134,23 @@ message DenomTrace {
|
||||
The `IBCDenom` function constructs the `Coin` denomination used when creating the ICS20 fungible token packet data:
|
||||
|
||||
```golang
|
||||
// Hash returns the hex bytes of the SHA256 hash of the DenomTrace fields.
|
||||
// Hash returns the hex bytes of the SHA256 hash of the DenomTrace fields using the following formula:
|
||||
//
|
||||
// hash = sha256(tracePath + "/" + baseDenom)
|
||||
func (dt DenomTrace) Hash() tmbytes.HexBytes {
|
||||
return tmhash.Sum(dt.Trace + "/" + dt.BaseDenom)
|
||||
return tmhash.Sum(dt.Path + "/" + dt.BaseDenom)
|
||||
}
|
||||
|
||||
// IBCDenom a coin denomination for an ICS20 fungible token in the format 'ibc/{hash(trace + baseDenom)}'. If the trace is empty, it will return the base denomination.
|
||||
// IBCDenom a coin denomination for an ICS20 fungible token in the format 'ibc/{hash(tracePath + baseDenom)}'.
|
||||
// If the trace is empty, it will return the base denomination.
|
||||
func (dt DenomTrace) IBCDenom() string {
|
||||
if dt.Trace != "" {
|
||||
if dt.Path != "" {
|
||||
return fmt.Sprintf("ibc/%s", dt.Hash())
|
||||
}
|
||||
return dt.BaseDenom
|
||||
}
|
||||
```
|
||||
|
||||
In order to trim the denomination trace prefix when sending/receiving fungible tokens, the `RemovePrefix` function is provided.
|
||||
|
||||
> NOTE: the prefix addition must be done on the client side.
|
||||
|
||||
```golang
|
||||
// RemovePrefix trims the first portID/channelID pair from the trace info. If the trace is already empty it will perform a no-op. If the trace is incorrectly constructed or doesn't have separators it will return an error.
|
||||
func (dt *DenomTrace) RemovePrefix() error {
|
||||
if dt.Trace == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
traceSplit := strings.SplitN(dt.Trace, "/", 3)
|
||||
|
||||
var err error
|
||||
switch {
|
||||
// NOTE: other cases are checked during msg validation
|
||||
case len(traceSplit) == 2:
|
||||
dt.Trace = ""
|
||||
case len(traceSplit) == 3:
|
||||
dt.Trace = traceSplit[2]
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### `x/ibc-transfer` Changes
|
||||
|
||||
In order to retrieve the trace information from an IBC denomination, a lookup table needs to be
|
||||
@ -217,44 +191,30 @@ hash, if the trace info is provided, or that the base denominations matches:
|
||||
```golang
|
||||
func (msg MsgTransfer) ValidateBasic() error {
|
||||
// ...
|
||||
if err := msg.Trace.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ValidateIBCDenom(msg.Token.Denom, msg.Trace); err != nil {
|
||||
return err
|
||||
}
|
||||
// ...
|
||||
return ValidateIBCDenom(msg.Token.Denom)
|
||||
}
|
||||
```
|
||||
|
||||
```golang
|
||||
// ValidateIBCDenom checks that the denomination for an IBC fungible token is valid. It returns error if the trace `hash` is invalid
|
||||
func ValidateIBCDenom(denom string, trace DenomTrace) error {
|
||||
// Validate that base denominations are equal if the trace info is not provided
|
||||
if trace.Trace == "" {
|
||||
if trace.BaseDenom != denom {
|
||||
return Wrapf(
|
||||
ErrInvalidDenomForTransfer,
|
||||
"token denom must match the trace base denom (%s ≠ %s)",
|
||||
denom, trace.BaseDenom,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateIBCDenom validates that the given denomination is either:
|
||||
//
|
||||
// - A valid base denomination (eg: 'uatom')
|
||||
// - A valid fungible token representation (i.e 'ibc/{hash}') per ADR 001 https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/adr-001-coin-source-tracing.md
|
||||
func ValidateIBCDenom(denom string) error {
|
||||
denomSplit := strings.SplitN(denom, "/", 2)
|
||||
|
||||
switch {
|
||||
case denomSplit[0] != "ibc":
|
||||
return Wrapf(ErrInvalidDenomForTransfer, "denomination should be prefixed with the format 'ibc/{hash(trace + \"/\" + %s)}'", denom)
|
||||
case len(denomSplit) == 2:
|
||||
return tmtypes.ValidateHash([]byte(denomSplit[1]))
|
||||
case strings.TrimSpace(denom) == "",
|
||||
len(denomSplit) == 1 && denomSplit[0] == "ibc",
|
||||
len(denomSplit) == 2 && (denomSplit[0] != "ibc" || strings.TrimSpace(denomSplit[1]) == ""):
|
||||
return sdkerrors.Wrapf(ErrInvalidDenomForTransfer, "denomination should be prefixed with the format 'ibc/{hash(trace + \"/\" + %s)}'", denom)
|
||||
|
||||
case denomSplit[0] == denom && strings.TrimSpace(denom) != "":
|
||||
return sdk.ValidateDenom(denom)
|
||||
}
|
||||
|
||||
denomTraceHash := denomSplit[1]
|
||||
traceHash := trace.Hash()
|
||||
if !bytes.Equal(traceHash.Bytes(), denomTraceHash.Bytes()) {
|
||||
return Errorf("token denomination trace hash mismatch, expected %s got %s", traceHash, denomTraceHash)
|
||||
if _, err := ParseHexHash(denomSplit[1]); err != nil {
|
||||
return Wrapf(err, "invalid denom trace hash %s", denomSplit[1])
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -266,6 +226,46 @@ The denomination trace info only needs to be updated when token is received:
|
||||
- Receiver is **source** chain: The receiver created the token and must have the trace lookup already stored (if necessary _ie_ native token case wouldn't need a lookup).
|
||||
- Receiver is **not source** chain: Store the received info. For example, during step 1, when chain `B` receives `transfer/channelToA/denom`.
|
||||
|
||||
```golang
|
||||
// SendTransfer
|
||||
// ...
|
||||
|
||||
fullDenomPath := token.Denom
|
||||
|
||||
// deconstruct the token denomination into the denomination trace info
|
||||
// to determine if the sender is the source chain
|
||||
if strings.HasPrefix(token.Denom, "ibc/") {
|
||||
fullDenomPath, err = k.DenomPathFromHash(ctx, token.Denom)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if types.SenderChainIsSource(sourcePort, sourceChannel, fullDenomPath) {
|
||||
//...
|
||||
```
|
||||
|
||||
```golang
|
||||
// DenomPathFromHash returns the full denomination path prefix from an ibc denom with a hash
|
||||
// component.
|
||||
func (k Keeper) DenomPathFromHash(ctx sdk.Context, denom string) (string, error) {
|
||||
hexHash := denom[4:]
|
||||
hash, err := ParseHexHash(hexHash)
|
||||
if err != nil {
|
||||
return "", Wrap(ErrInvalidDenomForTransfer, err.Error())
|
||||
}
|
||||
|
||||
denomTrace, found := k.GetDenomTrace(ctx, hash)
|
||||
if !found {
|
||||
return "", Wrap(ErrTraceNotFound, hexHash)
|
||||
}
|
||||
|
||||
fullDenomPath := denomTrace.GetFullDenomPath()
|
||||
return fullDenomPath, nil
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
```golang
|
||||
// OnRecvPacket
|
||||
// ...
|
||||
@ -277,19 +277,13 @@ The denomination trace info only needs to be updated when token is received:
|
||||
// NOTE: We use SourcePort and SourceChannel here, because the counterparty
|
||||
// chain would have prefixed with DestPort and DestChannel when originally
|
||||
// receiving this coin as seen in the "sender chain is the source" condition.
|
||||
voucherPrefix := GetDenomPrefix(packet.GetSourcePort(), packet.GetSourceChannel())
|
||||
|
||||
if ReceiverChainIsSource(packet.GetSourcePort(), packet.GetSourceChannel(), data.Denom) {
|
||||
// sender chain is not the source, unescrow tokens
|
||||
|
||||
// remove prefix added by sender chain
|
||||
if err := denomTrace.RemovePrefix(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// NOTE: since the sender is a sink chain, we already know the unprefixed denomination trace info
|
||||
|
||||
token := sdk.NewCoin(denomTrace.IBCDenom(), sdk.NewIntFromUint64(data.Amount))
|
||||
voucherPrefix := types.GetDenomPrefix(packet.GetSourcePort(), packet.GetSourceChannel())
|
||||
unprefixedDenom := data.Denom[len(voucherPrefix):]
|
||||
token := sdk.NewCoin(unprefixedDenom, sdk.NewIntFromUint64(data.Amount))
|
||||
|
||||
// unescrow tokens
|
||||
escrowAddress := types.GetEscrowAddress(packet.GetDestPort(), packet.GetDestChannel())
|
||||
@ -298,11 +292,13 @@ if ReceiverChainIsSource(packet.GetSourcePort(), packet.GetSourceChannel(), data
|
||||
|
||||
// sender chain is the source, mint vouchers
|
||||
|
||||
// construct the denomination trace from the full raw denomination
|
||||
denomTrace := NewDenomTraceFromRawDenom(data.Denom)
|
||||
// since SendPacket did not prefix the denomination, we must prefix denomination here
|
||||
sourcePrefix := types.GetDenomPrefix(packet.GetDestPort(), packet.GetDestChannel())
|
||||
// NOTE: sourcePrefix contains the trailing "/"
|
||||
prefixedDenom := sourcePrefix + data.Denom
|
||||
|
||||
// since SendPacket did not prefix the denomination with the voucherPrefix, we must add it here
|
||||
denomTrace.AddPrefix(packet.GetDestPort(), packet.GetDestChannel())
|
||||
// construct the denomination trace from the full raw denomination
|
||||
denomTrace := types.ParseDenomTrace(prefixedDenom)
|
||||
|
||||
// set the value to the lookup table if not stored already
|
||||
traceHash := denomTrace.Hash()
|
||||
@ -310,7 +306,8 @@ if !k.HasDenomTrace(ctx, traceHash) {
|
||||
k.SetDenomTrace(ctx, traceHash, denomTrace)
|
||||
}
|
||||
|
||||
voucher := sdk.NewCoin(denomTrace.IBCDenom(), sdk.NewIntFromUint64(data.Amount))
|
||||
voucherDenom := denomTrace.IBCDenom()
|
||||
voucher := sdk.NewCoin(voucherDenom, sdk.NewIntFromUint64(data.Amount))
|
||||
|
||||
// mint new tokens if the source of the transfer is the same chain
|
||||
if err := k.bankKeeper.MintCoins(
|
||||
@ -347,7 +344,8 @@ The coin denomination validation will need to be updated to reflect these change
|
||||
function will now:
|
||||
|
||||
- Accept slash separators (`"/"`) and uppercase characters (due to the `HexBytes` format)
|
||||
- Bump the maximum character length to 64
|
||||
- Bump the maximum character length to 128, as the hex representation used by Tendermint's
|
||||
`HexBytes` type contains 64 characters.
|
||||
|
||||
Additional validation logic, such as verifying the length of the hash, the may be added to the bank module in the future if the [custom base denomination validation](https://github.com/cosmos/cosmos-sdk/pull/6755) is integrated into the SDK.
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
## Status
|
||||
|
||||
> A decision may be "proposed" if the project stakeholders haven't agreed with it yet, or "accepted" once it is agreed. If a later ADR changes or reverses a decision, it may be marked as "deprecated" or "superseded" with a reference to its replacement.
|
||||
> {Deprecated|Proposed|Accepted}
|
||||
> {Deprecated|Proposed|Accepted} {Implemented|Not Implemented}
|
||||
|
||||
## Context
|
||||
|
||||
|
||||
@ -4,11 +4,16 @@ package ibc.transfer;
|
||||
option go_package = "github.com/cosmos/cosmos-sdk/x/ibc-transfer/types";
|
||||
|
||||
import "gogoproto/gogo.proto";
|
||||
import "ibc/transfer/transfer.proto";
|
||||
|
||||
// GenesisState is currently only used to ensure that the InitGenesis gets run
|
||||
// by the module manager
|
||||
message GenesisState {
|
||||
string port_id = 1 [
|
||||
(gogoproto.moretags) = "yaml:\"port_id\""
|
||||
];
|
||||
// GenesisState defines the ibc-transfer genesis state
|
||||
message GenesisState{
|
||||
string port_id = 1 [
|
||||
(gogoproto.moretags) = "yaml:\"port_id\""
|
||||
];
|
||||
repeated DenomTrace denom_traces = 2 [
|
||||
(gogoproto.castrepeated) = "Traces",
|
||||
(gogoproto.nullable) = false,
|
||||
(gogoproto.moretags) = "yaml:\"denom_traces\""
|
||||
];
|
||||
}
|
||||
|
||||
51
proto/ibc/transfer/query.proto
Normal file
51
proto/ibc/transfer/query.proto
Normal file
@ -0,0 +1,51 @@
|
||||
syntax = "proto3";
|
||||
package ibc.transfer;
|
||||
|
||||
import "gogoproto/gogo.proto";
|
||||
import "cosmos/base/query/v1beta1/pagination.proto";
|
||||
import "ibc/transfer/transfer.proto";
|
||||
import "google/api/annotations.proto";
|
||||
|
||||
option go_package = "github.com/cosmos/cosmos-sdk/x/ibc-transfer/types";
|
||||
|
||||
// Query provides defines the gRPC querier service.
|
||||
service Query {
|
||||
// DenomTrace queries a denomination trace information.
|
||||
rpc DenomTrace(QueryDenomTraceRequest) returns (QueryDenomTraceResponse) {
|
||||
option (google.api.http).get = "/ibc_transfer/v1beta1/denom_traces/{hash}";
|
||||
}
|
||||
|
||||
// DenomTraces queries all denomination traces.
|
||||
rpc DenomTraces(QueryDenomTracesRequest) returns (QueryDenomTracesResponse) {
|
||||
option (google.api.http).get = "/ibc_transfer/v1beta1/denom_traces";
|
||||
}
|
||||
}
|
||||
|
||||
// QueryDenomTraceRequest is the request type for the Query/DenomTrace RPC method
|
||||
message QueryDenomTraceRequest {
|
||||
// hash (in hex format) of the denomination trace information.
|
||||
string hash = 1;
|
||||
}
|
||||
|
||||
// QueryDenomTraceResponse is the response type for the Query/DenomTrace RPC method.
|
||||
message QueryDenomTraceResponse {
|
||||
// denom_trace returns the requested denomination trace information.
|
||||
DenomTrace denom_trace = 1;
|
||||
}
|
||||
|
||||
// QueryConnectionsRequest is the request type for the Query/DenomTraces RPC method
|
||||
message QueryDenomTracesRequest {
|
||||
// pagination defines an optional pagination for the request.
|
||||
cosmos.base.query.v1beta1.PageRequest pagination = 1;
|
||||
}
|
||||
|
||||
// QueryConnectionsResponse is the response type for the Query/DenomTraces RPC method.
|
||||
message QueryDenomTracesResponse {
|
||||
// denom_traces returns all denominations trace information.
|
||||
repeated DenomTrace denom_traces = 1 [
|
||||
(gogoproto.castrepeated) = "Traces",
|
||||
(gogoproto.nullable) = false
|
||||
];
|
||||
// pagination defines the pagination in the response.
|
||||
cosmos.base.query.v1beta1.PageResponse pagination = 2;
|
||||
}
|
||||
@ -17,8 +17,7 @@ message MsgTransfer {
|
||||
// the tokens to be transferred
|
||||
cosmos.base.v1beta1.Coin token = 3 [(gogoproto.nullable) = false];
|
||||
// the sender address
|
||||
bytes sender = 4
|
||||
[(gogoproto.casttype) = "github.com/cosmos/cosmos-sdk/types.AccAddress"];
|
||||
bytes sender = 4 [(gogoproto.casttype) = "github.com/cosmos/cosmos-sdk/types.AccAddress"];
|
||||
// the recipient address on the destination chain
|
||||
string receiver = 5;
|
||||
// Timeout height relative to the current block height.
|
||||
@ -26,8 +25,7 @@ message MsgTransfer {
|
||||
uint64 timeout_height = 6 [(gogoproto.moretags) = "yaml:\"timeout_height\""];
|
||||
// Timeout timestamp (in nanoseconds) relative to the current block timestamp.
|
||||
// The timeout is disabled when set to 0.
|
||||
uint64 timeout_timestamp = 7
|
||||
[(gogoproto.moretags) = "yaml:\"timeout_timestamp\""];
|
||||
uint64 timeout_timestamp = 7 [(gogoproto.moretags) = "yaml:\"timeout_timestamp\""];
|
||||
}
|
||||
|
||||
// FungibleTokenPacketData defines a struct for the packet payload
|
||||
@ -52,3 +50,13 @@ message FungibleTokenPacketAcknowledgement {
|
||||
bool success = 1;
|
||||
string error = 2;
|
||||
}
|
||||
|
||||
// DenomTrace contains the base denomination for ICS20 fungible tokens and the source tracing
|
||||
// information path.
|
||||
message DenomTrace {
|
||||
// path defines the chain of port/channel identifiers used for tracing the source of the fungible
|
||||
// token.
|
||||
string path = 1;
|
||||
// base denomination of the relayed fungible token.
|
||||
string base_denom = 2;
|
||||
}
|
||||
|
||||
@ -76,6 +76,7 @@ func TestCoinIsValid(t *testing.T) {
|
||||
{Coin{"ATOM", OneInt()}, true},
|
||||
{Coin{"a", OneInt()}, false},
|
||||
{Coin{loremIpsum, OneInt()}, false},
|
||||
{Coin{"ibc/7F1D3FCF4AE79E1554D670D1AD949A9BA4E4A3C76C63093E17E446A46061A7A2", OneInt()}, true},
|
||||
{Coin{"atOm", OneInt()}, true},
|
||||
{Coin{" ", OneInt()}, false},
|
||||
}
|
||||
@ -632,6 +633,8 @@ func TestParse(t *testing.T) {
|
||||
{"1.2btc", false, nil}, // amount must be integer
|
||||
{"5foo:bar", false, nil}, // invalid separator
|
||||
{"10atom10", true, Coins{{"atom10", NewInt(10)}}},
|
||||
{"200transfer/channelToA/uatom", true, Coins{{"transfer/channelToA/uatom", NewInt(200)}}},
|
||||
{"50ibc/7F1D3FCF4AE79E1554D670D1AD949A9BA4E4A3C76C63093E17E446A46061A7A2", true, Coins{{"ibc/7F1D3FCF4AE79E1554D670D1AD949A9BA4E4A3C76C63093E17E446A46061A7A2", NewInt(50)}}},
|
||||
}
|
||||
|
||||
for tcIndex, tc := range cases {
|
||||
|
||||
@ -6,6 +6,23 @@ import (
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
)
|
||||
|
||||
// GetQueryCmd returns the query commands for IBC connections
|
||||
func GetQueryCmd() *cobra.Command {
|
||||
queryCmd := &cobra.Command{
|
||||
Use: "ibc-transfer",
|
||||
Short: "IBC fungible token transfer query subcommands",
|
||||
DisableFlagParsing: true,
|
||||
SuggestionsMinimumDistance: 2,
|
||||
}
|
||||
|
||||
queryCmd.AddCommand(
|
||||
GetCmdQueryDenomTrace(),
|
||||
GetCmdQueryDenomTraces(),
|
||||
)
|
||||
|
||||
return queryCmd
|
||||
}
|
||||
|
||||
// NewTxCmd returns the transaction commands for IBC fungible token transfer
|
||||
func NewTxCmd() *cobra.Command {
|
||||
txCmd := &cobra.Command{
|
||||
|
||||
87
x/ibc-transfer/client/cli/query.go
Normal file
87
x/ibc-transfer/client/cli/query.go
Normal file
@ -0,0 +1,87 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/client/flags"
|
||||
"github.com/cosmos/cosmos-sdk/version"
|
||||
"github.com/cosmos/cosmos-sdk/x/ibc-transfer/types"
|
||||
)
|
||||
|
||||
// GetCmdQueryDenomTrace defines the command to query a a denomination trace from a given hash.
|
||||
func GetCmdQueryDenomTrace() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "denom-trace [hash]",
|
||||
Short: "Query the denom trace info from a given trace hash",
|
||||
Long: "Query the denom trace info from a given trace hash",
|
||||
Example: fmt.Sprintf("%s query ibc-transfer denom-trace [hash]", version.AppName),
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx := client.GetClientContextFromCmd(cmd)
|
||||
clientCtx, err := client.ReadQueryCommandFlags(clientCtx, cmd.Flags())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
queryClient := types.NewQueryClient(clientCtx)
|
||||
|
||||
req := &types.QueryDenomTraceRequest{
|
||||
Hash: args[0],
|
||||
}
|
||||
|
||||
res, err := queryClient.DenomTrace(context.Background(), req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return clientCtx.PrintOutput(res)
|
||||
},
|
||||
}
|
||||
|
||||
flags.AddQueryFlagsToCmd(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// GetCmdQueryDenomTraces defines the command to query all the denomination trace infos
|
||||
// that this chain mantains.
|
||||
func GetCmdQueryDenomTraces() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "denom-traces",
|
||||
Short: "Query the trace info for all token denominations",
|
||||
Long: "Query the trace info for all token denominations",
|
||||
Example: fmt.Sprintf("%s query ibc-transfer denom-traces", version.AppName),
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
clientCtx := client.GetClientContextFromCmd(cmd)
|
||||
clientCtx, err := client.ReadQueryCommandFlags(clientCtx, cmd.Flags())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
queryClient := types.NewQueryClient(clientCtx)
|
||||
|
||||
pageReq, err := client.ReadPageRequest(cmd.Flags())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := &types.QueryDenomTracesRequest{
|
||||
Pagination: pageReq,
|
||||
}
|
||||
|
||||
res, err := queryClient.DenomTraces(context.Background(), req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return clientCtx.PrintOutput(res)
|
||||
},
|
||||
}
|
||||
flags.AddQueryFlagsToCmd(cmd)
|
||||
flags.AddPaginationFlagsToCmd(cmd, "denominations trace")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@ -49,6 +49,11 @@ to the counterparty channel. Any timeout set to 0 is disabled.`),
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(coin.Denom, "ibc/") {
|
||||
denomTrace := types.ParseDenomTrace(coin.Denom)
|
||||
coin.Denom = denomTrace.IBCDenom()
|
||||
}
|
||||
|
||||
timeoutHeight, err := cmd.Flags().GetUint64(flagPacketTimeoutHeight)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@ -21,7 +21,7 @@ func RegisterRoutes(clientCtx client.Context, r *mux.Router) {
|
||||
// TransferTxReq defines the properties of a transfer tx request's body.
|
||||
type TransferTxReq struct {
|
||||
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||
Amount sdk.Coin `json:"amount" yaml:"amount"`
|
||||
Token sdk.Coin `json:"token" yaml:"token"`
|
||||
Receiver string `json:"receiver" yaml:"receiver"`
|
||||
TimeoutHeight uint64 `json:"timeout_height" yaml:"timeout_height"`
|
||||
TimeoutTimestamp uint64 `json:"timeout_timestamp" yaml:"timeout_timestamp"`
|
||||
|
||||
@ -56,7 +56,7 @@ func transferHandlerFn(clientCtx client.Context) http.HandlerFunc {
|
||||
msg := types.NewMsgTransfer(
|
||||
portID,
|
||||
channelID,
|
||||
req.Amount,
|
||||
req.Token,
|
||||
fromAddr,
|
||||
req.Receiver,
|
||||
req.TimeoutHeight,
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
package transfer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/ibc-transfer/keeper"
|
||||
"github.com/cosmos/cosmos-sdk/x/ibc-transfer/types"
|
||||
)
|
||||
|
||||
// InitGenesis binds to portid from genesis state
|
||||
func InitGenesis(ctx sdk.Context, keeper keeper.Keeper, state types.GenesisState) {
|
||||
keeper.SetPort(ctx, state.PortId)
|
||||
|
||||
// Only try to bind to port if it is not already bound, since we may already own
|
||||
// port capability from capability InitGenesis
|
||||
if !keeper.IsBound(ctx, state.PortId) {
|
||||
// transfer module binds to the transfer port on InitChain
|
||||
// and claims the returned capability
|
||||
err := keeper.BindPort(ctx, state.PortId)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("could not claim port capability: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// check if the module account exists
|
||||
moduleAcc := keeper.GetTransferAccount(ctx)
|
||||
if moduleAcc == nil {
|
||||
panic(fmt.Sprintf("%s module account has not been set", types.ModuleName))
|
||||
}
|
||||
}
|
||||
|
||||
// ExportGenesis exports transfer module's portID into its geneis state
|
||||
func ExportGenesis(ctx sdk.Context, keeper keeper.Keeper) *types.GenesisState {
|
||||
return &types.GenesisState{
|
||||
PortId: keeper.GetPort(ctx),
|
||||
}
|
||||
}
|
||||
@ -29,7 +29,7 @@ func handleMsgTransfer(ctx sdk.Context, k keeper.Keeper, msg *types.MsgTransfer)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
k.Logger(ctx).Info("IBC transfer: %s from %s to %s", msg.Token, msg.Sender, msg.Receiver)
|
||||
k.Logger(ctx).Info("IBC fungible token transfer", "token", msg.Token, "sender", msg.Sender, "receiver", msg.Receiver)
|
||||
|
||||
ctx.EventManager().EmitEvents(sdk.Events{
|
||||
sdk.NewEvent(
|
||||
|
||||
@ -48,9 +48,9 @@ func (suite *HandlerTestSuite) TestHandleMsgTransfer() {
|
||||
err = suite.coordinator.RelayPacket(suite.chainA, suite.chainB, clientA, clientB, packet, ack.GetBytes())
|
||||
suite.Require().NoError(err) // relay committed
|
||||
|
||||
// check that voucher exists on chain
|
||||
voucherDenom := types.GetPrefixedDenom(packet.GetDestPort(), packet.GetDestChannel(), sdk.DefaultBondDenom)
|
||||
balance := suite.chainB.App.BankKeeper.GetBalance(suite.chainB.GetContext(), suite.chainB.SenderAccount.GetAddress(), voucherDenom)
|
||||
// check that voucher exists on chain B
|
||||
voucherDenomTrace := types.ParseDenomTrace(types.GetPrefixedDenom(packet.GetDestPort(), packet.GetDestChannel(), sdk.DefaultBondDenom))
|
||||
balance := suite.chainB.App.BankKeeper.GetBalance(suite.chainB.GetContext(), suite.chainB.SenderAccount.GetAddress(), voucherDenomTrace.IBCDenom())
|
||||
|
||||
coinToSendBackToA := types.GetTransferCoin(channelB.PortID, channelB.ID, sdk.DefaultBondDenom, 100)
|
||||
suite.Require().Equal(coinToSendBackToA, balance)
|
||||
@ -62,7 +62,9 @@ func (suite *HandlerTestSuite) TestHandleMsgTransfer() {
|
||||
suite.Require().NoError(err) // message committed
|
||||
|
||||
// relay send
|
||||
fungibleTokenPacket = types.NewFungibleTokenPacketData(coinToSendBackToA.Denom, coinToSendBackToA.Amount.Uint64(), suite.chainB.SenderAccount.GetAddress().String(), suite.chainA.SenderAccount.GetAddress().String())
|
||||
// NOTE: fungible token is prefixed with the full trace in order to verify the packet commitment
|
||||
voucherDenom := voucherDenomTrace.GetPrefix() + voucherDenomTrace.BaseDenom
|
||||
fungibleTokenPacket = types.NewFungibleTokenPacketData(voucherDenom, coinToSendBackToA.Amount.Uint64(), suite.chainB.SenderAccount.GetAddress().String(), suite.chainA.SenderAccount.GetAddress().String())
|
||||
packet = channeltypes.NewPacket(fungibleTokenPacket.GetBytes(), 1, channelB.PortID, channelB.ID, channelA.PortID, channelA.ID, 110, 0)
|
||||
err = suite.coordinator.RelayPacket(suite.chainB, suite.chainA, clientB, clientA, packet, ack.GetBytes())
|
||||
suite.Require().NoError(err) // relay committed
|
||||
@ -76,6 +78,10 @@ func (suite *HandlerTestSuite) TestHandleMsgTransfer() {
|
||||
escrowAddress := types.GetEscrowAddress(packet.GetDestPort(), packet.GetDestChannel())
|
||||
balance = suite.chainA.App.BankKeeper.GetBalance(suite.chainA.GetContext(), escrowAddress, sdk.DefaultBondDenom)
|
||||
suite.Require().Equal(sdk.NewCoin(sdk.DefaultBondDenom, sdk.ZeroInt()), balance)
|
||||
|
||||
// check that balance on chain B is empty
|
||||
balance = suite.chainB.App.BankKeeper.GetBalance(suite.chainB.GetContext(), suite.chainB.SenderAccount.GetAddress(), voucherDenomTrace.IBCDenom())
|
||||
suite.Require().Zero(balance.Amount.Int64())
|
||||
}
|
||||
|
||||
func TestHandlerTestSuite(t *testing.T) {
|
||||
|
||||
42
x/ibc-transfer/keeper/genesis.go
Normal file
42
x/ibc-transfer/keeper/genesis.go
Normal file
@ -0,0 +1,42 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/ibc-transfer/types"
|
||||
)
|
||||
|
||||
// InitGenesis initializes the ibc-transfer state and binds to PortID.
|
||||
func (k Keeper) InitGenesis(ctx sdk.Context, state types.GenesisState) {
|
||||
k.SetPort(ctx, state.PortId)
|
||||
|
||||
for _, trace := range state.DenomTraces {
|
||||
k.SetDenomTrace(ctx, trace)
|
||||
}
|
||||
|
||||
// Only try to bind to port if it is not already bound, since we may already own
|
||||
// port capability from capability InitGenesis
|
||||
if !k.IsBound(ctx, state.PortId) {
|
||||
// transfer module binds to the transfer port on InitChain
|
||||
// and claims the returned capability
|
||||
err := k.BindPort(ctx, state.PortId)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("could not claim port capability: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// check if the module account exists
|
||||
moduleAcc := k.GetTransferAccount(ctx)
|
||||
if moduleAcc == nil {
|
||||
panic(fmt.Sprintf("%s module account has not been set", types.ModuleName))
|
||||
}
|
||||
}
|
||||
|
||||
// ExportGenesis exports ibc-transfer module's portID and denom trace info into its genesis state.
|
||||
func (k Keeper) ExportGenesis(ctx sdk.Context) *types.GenesisState {
|
||||
return &types.GenesisState{
|
||||
PortId: k.GetPort(ctx),
|
||||
DenomTraces: k.GetAllDenomTraces(ctx),
|
||||
}
|
||||
}
|
||||
39
x/ibc-transfer/keeper/genesis_test.go
Normal file
39
x/ibc-transfer/keeper/genesis_test.go
Normal file
@ -0,0 +1,39 @@
|
||||
package keeper_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/x/ibc-transfer/types"
|
||||
)
|
||||
|
||||
func (suite *KeeperTestSuite) TestGenesis() {
|
||||
var (
|
||||
path string
|
||||
traces types.Traces
|
||||
)
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
prefix := fmt.Sprintf("transfer/channelToChain%d", i)
|
||||
if i == 0 {
|
||||
path = prefix
|
||||
} else {
|
||||
path = prefix + "/" + path
|
||||
}
|
||||
|
||||
denomTrace := types.DenomTrace{
|
||||
BaseDenom: "uatom",
|
||||
Path: path,
|
||||
}
|
||||
traces = append(types.Traces{denomTrace}, traces...)
|
||||
suite.chainA.App.TransferKeeper.SetDenomTrace(suite.chainA.GetContext(), denomTrace)
|
||||
}
|
||||
|
||||
genesis := suite.chainA.App.TransferKeeper.ExportGenesis(suite.chainA.GetContext())
|
||||
|
||||
suite.Require().Equal(types.PortID, genesis.PortId)
|
||||
suite.Require().Equal(traces.Sort(), genesis.DenomTraces)
|
||||
|
||||
suite.Require().NotPanics(func() {
|
||||
suite.chainA.App.TransferKeeper.InitGenesis(suite.chainA.GetContext(), *genesis)
|
||||
})
|
||||
}
|
||||
73
x/ibc-transfer/keeper/grpc_query.go
Normal file
73
x/ibc-transfer/keeper/grpc_query.go
Normal file
@ -0,0 +1,73 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/store/prefix"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
"github.com/cosmos/cosmos-sdk/types/query"
|
||||
"github.com/cosmos/cosmos-sdk/x/ibc-transfer/types"
|
||||
)
|
||||
|
||||
var _ types.QueryServer = Keeper{}
|
||||
|
||||
// DenomTrace implements the Query/DenomTrace gRPC method
|
||||
func (q Keeper) DenomTrace(c context.Context, req *types.QueryDenomTraceRequest) (*types.QueryDenomTraceResponse, error) {
|
||||
if req == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "empty request")
|
||||
}
|
||||
|
||||
hash, err := types.ParseHexHash(req.Hash)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("invalid denom trace hash %s, %s", req.Hash, err))
|
||||
}
|
||||
|
||||
ctx := sdk.UnwrapSDKContext(c)
|
||||
denomTrace, found := q.GetDenomTrace(ctx, hash)
|
||||
if !found {
|
||||
return nil, status.Error(
|
||||
codes.NotFound,
|
||||
sdkerrors.Wrap(types.ErrTraceNotFound, req.Hash).Error(),
|
||||
)
|
||||
}
|
||||
|
||||
return &types.QueryDenomTraceResponse{
|
||||
DenomTrace: &denomTrace,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DenomTraces implements the Query/DenomTraces gRPC method
|
||||
func (q Keeper) DenomTraces(c context.Context, req *types.QueryDenomTracesRequest) (*types.QueryDenomTracesResponse, error) {
|
||||
if req == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "empty request")
|
||||
}
|
||||
|
||||
ctx := sdk.UnwrapSDKContext(c)
|
||||
|
||||
traces := types.Traces{}
|
||||
store := prefix.NewStore(ctx.KVStore(q.storeKey), types.DenomTraceKey)
|
||||
|
||||
pageRes, err := query.Paginate(store, req.Pagination, func(_, value []byte) error {
|
||||
var result types.DenomTrace
|
||||
if err := q.cdc.UnmarshalBinaryBare(value, &result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
traces = append(traces, result)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.QueryDenomTracesResponse{
|
||||
DenomTraces: traces,
|
||||
Pagination: pageRes,
|
||||
}, nil
|
||||
}
|
||||
135
x/ibc-transfer/keeper/grpc_query_test.go
Normal file
135
x/ibc-transfer/keeper/grpc_query_test.go
Normal file
@ -0,0 +1,135 @@
|
||||
package keeper_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/query"
|
||||
"github.com/cosmos/cosmos-sdk/x/ibc-transfer/types"
|
||||
)
|
||||
|
||||
func (suite *KeeperTestSuite) TestQueryConnection() {
|
||||
var (
|
||||
req *types.QueryDenomTraceRequest
|
||||
expTrace types.DenomTrace
|
||||
)
|
||||
|
||||
testCases := []struct {
|
||||
msg string
|
||||
malleate func()
|
||||
expPass bool
|
||||
}{
|
||||
{
|
||||
"invalid hex hash",
|
||||
func() {
|
||||
req = &types.QueryDenomTraceRequest{
|
||||
Hash: "!@#!@#!",
|
||||
}
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"not found denom trace",
|
||||
func() {
|
||||
expTrace.Path = "transfer/channelToA/transfer/channelToB"
|
||||
expTrace.BaseDenom = "uatom"
|
||||
req = &types.QueryDenomTraceRequest{
|
||||
Hash: expTrace.Hash().String(),
|
||||
}
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"success",
|
||||
func() {
|
||||
expTrace.Path = "transfer/channelToA/transfer/channelToB"
|
||||
expTrace.BaseDenom = "uatom"
|
||||
suite.chainA.App.TransferKeeper.SetDenomTrace(suite.chainA.GetContext(), expTrace)
|
||||
|
||||
req = &types.QueryDenomTraceRequest{
|
||||
Hash: expTrace.Hash().String(),
|
||||
}
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
suite.Run(fmt.Sprintf("Case %s", tc.msg), func() {
|
||||
suite.SetupTest() // reset
|
||||
|
||||
tc.malleate()
|
||||
ctx := sdk.WrapSDKContext(suite.chainA.GetContext())
|
||||
|
||||
res, err := suite.queryClient.DenomTrace(ctx, req)
|
||||
|
||||
if tc.expPass {
|
||||
suite.Require().NoError(err)
|
||||
suite.Require().NotNil(res)
|
||||
suite.Require().Equal(&expTrace, res.DenomTrace)
|
||||
} else {
|
||||
suite.Require().Error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *KeeperTestSuite) TestQueryConnections() {
|
||||
var (
|
||||
req *types.QueryDenomTracesRequest
|
||||
expTraces = types.Traces(nil)
|
||||
)
|
||||
|
||||
testCases := []struct {
|
||||
msg string
|
||||
malleate func()
|
||||
expPass bool
|
||||
}{
|
||||
{
|
||||
"empty pagination",
|
||||
func() {
|
||||
req = &types.QueryDenomTracesRequest{}
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"success",
|
||||
func() {
|
||||
expTraces = append(expTraces, types.DenomTrace{Path: "", BaseDenom: "uatom"})
|
||||
expTraces = append(expTraces, types.DenomTrace{Path: "transfer/channelToB", BaseDenom: "uatom"})
|
||||
expTraces = append(expTraces, types.DenomTrace{Path: "transfer/channelToA/transfer/channelToB", BaseDenom: "uatom"})
|
||||
|
||||
for _, trace := range expTraces {
|
||||
suite.chainA.App.TransferKeeper.SetDenomTrace(suite.chainA.GetContext(), trace)
|
||||
}
|
||||
|
||||
req = &types.QueryDenomTracesRequest{
|
||||
Pagination: &query.PageRequest{
|
||||
Limit: 5,
|
||||
CountTotal: false,
|
||||
},
|
||||
}
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
suite.Run(fmt.Sprintf("Case %s", tc.msg), func() {
|
||||
suite.SetupTest() // reset
|
||||
|
||||
tc.malleate()
|
||||
ctx := sdk.WrapSDKContext(suite.chainA.GetContext())
|
||||
|
||||
res, err := suite.queryClient.DenomTraces(ctx, req)
|
||||
|
||||
if tc.expPass {
|
||||
suite.Require().NoError(err)
|
||||
suite.Require().NotNil(res)
|
||||
suite.Require().Equal(expTraces.Sort(), res.DenomTraces)
|
||||
} else {
|
||||
suite.Require().Error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -3,9 +3,11 @@ package keeper
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
tmbytes "github.com/tendermint/tendermint/libs/bytes"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
"github.com/cosmos/cosmos-sdk/store/prefix"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
|
||||
@ -109,6 +111,61 @@ func (k Keeper) SetPort(ctx sdk.Context, portID string) {
|
||||
store.Set(types.PortKey, []byte(portID))
|
||||
}
|
||||
|
||||
// GetDenomTrace retreives the full identifiers trace and base denomination from the store.
|
||||
func (k Keeper) GetDenomTrace(ctx sdk.Context, denomTraceHash tmbytes.HexBytes) (types.DenomTrace, bool) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.DenomTraceKey)
|
||||
bz := store.Get(denomTraceHash)
|
||||
if bz == nil {
|
||||
return types.DenomTrace{}, false
|
||||
}
|
||||
|
||||
var denomTrace types.DenomTrace
|
||||
k.cdc.MustUnmarshalBinaryBare(bz, &denomTrace)
|
||||
return denomTrace, true
|
||||
}
|
||||
|
||||
// HasDenomTrace checks if a the key with the given denomination trace hash exists on the store.
|
||||
func (k Keeper) HasDenomTrace(ctx sdk.Context, denomTraceHash tmbytes.HexBytes) bool {
|
||||
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.DenomTraceKey)
|
||||
return store.Has(denomTraceHash)
|
||||
}
|
||||
|
||||
// SetDenomTrace sets a new {trace hash -> denom trace} pair to the store.
|
||||
func (k Keeper) SetDenomTrace(ctx sdk.Context, denomTrace types.DenomTrace) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.DenomTraceKey)
|
||||
bz := k.cdc.MustMarshalBinaryBare(&denomTrace)
|
||||
store.Set(denomTrace.Hash(), bz)
|
||||
}
|
||||
|
||||
// GetAllDenomTraces returns the trace information for all the denominations.
|
||||
func (k Keeper) GetAllDenomTraces(ctx sdk.Context) types.Traces {
|
||||
traces := types.Traces{}
|
||||
k.IterateDenomTraces(ctx, func(denomTrace types.DenomTrace) bool {
|
||||
traces = append(traces, denomTrace)
|
||||
return false
|
||||
})
|
||||
|
||||
return traces.Sort()
|
||||
}
|
||||
|
||||
// IterateDenomTraces iterates over the denomination traces in the store
|
||||
// and performs a callback function.
|
||||
func (k Keeper) IterateDenomTraces(ctx sdk.Context, cb func(denomTrace types.DenomTrace) bool) {
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
iterator := sdk.KVStorePrefixIterator(store, types.DenomTraceKey)
|
||||
|
||||
defer iterator.Close()
|
||||
for ; iterator.Valid(); iterator.Next() {
|
||||
|
||||
var denomTrace types.DenomTrace
|
||||
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &denomTrace)
|
||||
|
||||
if cb(denomTrace) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ClaimCapability allows the transfer module that can claim a capability that IBC module
|
||||
// passes to it
|
||||
func (k Keeper) ClaimCapability(ctx sdk.Context, cap *capabilitytypes.Capability, name string) error {
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/baseapp"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/ibc-transfer/types"
|
||||
ibctesting "github.com/cosmos/cosmos-sdk/x/ibc/testing"
|
||||
@ -19,12 +20,18 @@ type KeeperTestSuite struct {
|
||||
// testing chains used for convenience and readability
|
||||
chainA *ibctesting.TestChain
|
||||
chainB *ibctesting.TestChain
|
||||
|
||||
queryClient types.QueryClient
|
||||
}
|
||||
|
||||
func (suite *KeeperTestSuite) SetupTest() {
|
||||
suite.coordinator = ibctesting.NewCoordinator(suite.T(), 2)
|
||||
suite.chainA = suite.coordinator.GetChain(ibctesting.GetChainID(0))
|
||||
suite.chainB = suite.coordinator.GetChain(ibctesting.GetChainID(1))
|
||||
|
||||
queryHelper := baseapp.NewQueryServerTestHelper(suite.chainA.GetContext(), suite.chainA.App.InterfaceRegistry())
|
||||
types.RegisterQueryServer(queryHelper, suite.chainA.App.TransferKeeper)
|
||||
suite.queryClient = types.NewQueryClient(queryHelper)
|
||||
}
|
||||
|
||||
func (suite *KeeperTestSuite) TestGetTransferAccount() {
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
"github.com/cosmos/cosmos-sdk/x/ibc-transfer/types"
|
||||
@ -40,7 +42,6 @@ import (
|
||||
// 4. A -> C : sender chain is sink zone. Denom upon receiving: 'C/B/denom'
|
||||
// 5. C -> B : sender chain is sink zone. Denom upon receiving: 'B/denom'
|
||||
// 6. B -> A : sender chain is sink zone. Denom upon receiving: 'denom'
|
||||
|
||||
func (k Keeper) SendTransfer(
|
||||
ctx sdk.Context,
|
||||
sourcePort,
|
||||
@ -70,17 +71,30 @@ func (k Keeper) SendTransfer(
|
||||
|
||||
// begin createOutgoingPacket logic
|
||||
// See spec for this logic: https://github.com/cosmos/ics/tree/master/spec/ics-020-fungible-token-transfer#packet-relay
|
||||
|
||||
channelCap, ok := k.scopedKeeper.GetCapability(ctx, host.ChannelCapabilityPath(sourcePort, sourceChannel))
|
||||
if !ok {
|
||||
return sdkerrors.Wrap(channeltypes.ErrChannelCapabilityNotFound, "module does not own channel capability")
|
||||
}
|
||||
|
||||
// NOTE: denomination and hex hash correctness checked during msg.ValidateBasic
|
||||
fullDenomPath := token.Denom
|
||||
|
||||
var err error
|
||||
|
||||
// deconstruct the token denomination into the denomination trace info
|
||||
// to determine if the sender is the source chain
|
||||
if strings.HasPrefix(token.Denom, "ibc/") {
|
||||
fullDenomPath, err = k.DenomPathFromHash(ctx, token.Denom)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: SendTransfer simply sends the denomination as it exists on its own
|
||||
// chain inside the packet data. The receiving chain will perform denom
|
||||
// prefixing as necessary.
|
||||
|
||||
if types.SenderChainIsSource(sourcePort, sourceChannel, token.Denom) {
|
||||
if types.SenderChainIsSource(sourcePort, sourceChannel, fullDenomPath) {
|
||||
// create the escrow address for the tokens
|
||||
escrowAddress := types.GetEscrowAddress(sourcePort, sourceChannel)
|
||||
|
||||
@ -110,7 +124,7 @@ func (k Keeper) SendTransfer(
|
||||
}
|
||||
|
||||
packetData := types.NewFungibleTokenPacketData(
|
||||
token.Denom, token.Amount.Uint64(), sender.String(), receiver,
|
||||
fullDenomPath, token.Amount.Uint64(), sender.String(), receiver,
|
||||
)
|
||||
|
||||
packet := channeltypes.NewPacket(
|
||||
@ -133,7 +147,10 @@ func (k Keeper) SendTransfer(
|
||||
// back tokens this chain originally transferred to it, the tokens are
|
||||
// unescrowed and sent to the receiving address.
|
||||
func (k Keeper) OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet, data types.FungibleTokenPacketData) error {
|
||||
// NOTE: packet data type already checked in handler.go
|
||||
// validate packet data upon receiving
|
||||
if err := data.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// decode the receiver address
|
||||
receiver, err := sdk.AccAddressFromBech32(data.Receiver)
|
||||
@ -168,7 +185,25 @@ func (k Keeper) OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet, data t
|
||||
sourcePrefix := types.GetDenomPrefix(packet.GetDestPort(), packet.GetDestChannel())
|
||||
// NOTE: sourcePrefix contains the trailing "/"
|
||||
prefixedDenom := sourcePrefix + data.Denom
|
||||
voucher := sdk.NewCoin(prefixedDenom, sdk.NewIntFromUint64(data.Amount))
|
||||
|
||||
// construct the denomination trace from the full raw denomination
|
||||
denomTrace := types.ParseDenomTrace(prefixedDenom)
|
||||
|
||||
traceHash := denomTrace.Hash()
|
||||
if !k.HasDenomTrace(ctx, traceHash) {
|
||||
k.SetDenomTrace(ctx, denomTrace)
|
||||
}
|
||||
|
||||
voucherDenom := denomTrace.IBCDenom()
|
||||
ctx.EventManager().EmitEvent(
|
||||
sdk.NewEvent(
|
||||
types.EventTypeDenomTrace,
|
||||
sdk.NewAttribute(types.AttributeKeyTraceHash, traceHash.String()),
|
||||
sdk.NewAttribute(types.AttributeKeyDenom, voucherDenom),
|
||||
),
|
||||
)
|
||||
|
||||
voucher := sdk.NewCoin(voucherDenom, sdk.NewIntFromUint64(data.Amount))
|
||||
|
||||
// mint new tokens if the source of the transfer is the same chain
|
||||
if err := k.bankKeeper.MintCoins(
|
||||
@ -230,3 +265,21 @@ func (k Keeper) refundPacketToken(ctx sdk.Context, packet channeltypes.Packet, d
|
||||
|
||||
return k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, sender, sdk.NewCoins(token))
|
||||
}
|
||||
|
||||
// DenomPathFromHash returns the full denomination path prefix from an ibc denom with a hash
|
||||
// component.
|
||||
func (k Keeper) DenomPathFromHash(ctx sdk.Context, denom string) (string, error) {
|
||||
hexHash := denom[4:]
|
||||
hash, err := types.ParseHexHash(hexHash)
|
||||
if err != nil {
|
||||
return "", sdkerrors.Wrap(types.ErrInvalidDenomForTransfer, err.Error())
|
||||
}
|
||||
|
||||
denomTrace, found := k.GetDenomTrace(ctx, hash)
|
||||
if !found {
|
||||
return "", sdkerrors.Wrap(types.ErrTraceNotFound, hexHash)
|
||||
}
|
||||
|
||||
fullDenomPath := denomTrace.GetFullDenomPath()
|
||||
return fullDenomPath, nil
|
||||
}
|
||||
|
||||
@ -75,7 +75,7 @@ func (AppModuleBasic) GetTxCmd() *cobra.Command {
|
||||
|
||||
// GetQueryCmd implements AppModuleBasic interface
|
||||
func (AppModuleBasic) GetQueryCmd() *cobra.Command {
|
||||
return nil
|
||||
return cli.GetQueryCmd()
|
||||
}
|
||||
|
||||
// RegisterInterfaces registers module concrete types into protobuf Any.
|
||||
@ -118,21 +118,23 @@ func (am AppModule) LegacyQuerierHandler(codec.JSONMarshaler) sdk.Querier {
|
||||
|
||||
// RegisterQueryService registers a GRPC query service to respond to the
|
||||
// module-specific GRPC queries.
|
||||
func (am AppModule) RegisterQueryService(grpc.Server) {}
|
||||
func (am AppModule) RegisterQueryService(server grpc.Server) {
|
||||
types.RegisterQueryServer(server, am.keeper)
|
||||
}
|
||||
|
||||
// InitGenesis performs genesis initialization for the ibc transfer module. It returns
|
||||
// InitGenesis performs genesis initialization for the ibc-transfer module. It returns
|
||||
// no validator updates.
|
||||
func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONMarshaler, data json.RawMessage) []abci.ValidatorUpdate {
|
||||
var genesisState types.GenesisState
|
||||
cdc.MustUnmarshalJSON(data, &genesisState)
|
||||
|
||||
// TODO: check if the IBC transfer module account is set
|
||||
InitGenesis(ctx, am.keeper, genesisState)
|
||||
am.keeper.InitGenesis(ctx, genesisState)
|
||||
return []abci.ValidatorUpdate{}
|
||||
}
|
||||
|
||||
// ExportGenesis returns the exported genesis state as raw bytes for the ibc-transfer
|
||||
// module.
|
||||
func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONMarshaler) json.RawMessage {
|
||||
gs := ExportGenesis(ctx, am.keeper)
|
||||
gs := am.keeper.ExportGenesis(ctx)
|
||||
return cdc.MustMarshalJSON(gs)
|
||||
}
|
||||
|
||||
@ -288,10 +290,12 @@ func (am AppModule) OnRecvPacket(
|
||||
if err := types.ModuleCdc.UnmarshalJSON(packet.GetData(), &data); err != nil {
|
||||
return nil, nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "cannot unmarshal ICS-20 transfer packet data: %s", err.Error())
|
||||
}
|
||||
|
||||
acknowledgement := types.FungibleTokenPacketAcknowledgement{
|
||||
Success: true,
|
||||
Error: "",
|
||||
}
|
||||
|
||||
if err := am.keeper.OnRecvPacket(ctx, packet, data); err != nil {
|
||||
acknowledgement = types.FungibleTokenPacketAcknowledgement{
|
||||
Success: false,
|
||||
|
||||
@ -4,7 +4,7 @@ order: 4
|
||||
|
||||
# Messages
|
||||
|
||||
### MsgTransfer
|
||||
## MsgTransfer
|
||||
|
||||
A fungible token cross chain transfer is achieved by using the `MsgTransfer`:
|
||||
|
||||
@ -29,6 +29,7 @@ This message is expected to fail if:
|
||||
- `Sender` is empty
|
||||
- `Receiver` is empty
|
||||
- `TimeoutHeight` and `TimeoutTimestamp` are both zero
|
||||
- `Token.Denom` is not a valid IBC denomination as per [ADR 001 - Coin Source Tracing](./../../../docs/architecture/adr-001-coin-source-tracing.md).
|
||||
|
||||
This message will send a fungible token to the counterparty chain represented
|
||||
by the counterparty Channel End connected to the Channel End with the identifiers
|
||||
@ -37,4 +38,3 @@ by the counterparty Channel End connected to the Channel End with the identifier
|
||||
The denomination provided for transfer should correspond to the same denomination
|
||||
represented on this chain. The prefixes will be added as necessary upon by the
|
||||
receiving chain.
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ order: 5
|
||||
| fungible_token_packet | receiver | {receiver} |
|
||||
| fungible_token_packet | denom | {denom} |
|
||||
| fungible_token_packet | amount | {amount} |
|
||||
| denomination_trace | trace_hash | {hex_hash} |
|
||||
|
||||
## OnAcknowledgePacket callback
|
||||
|
||||
@ -40,8 +41,3 @@ order: 5
|
||||
| fungible_token_packet | refund_receiver | {receiver} |
|
||||
| fungible_token_packet | denom | {denom} |
|
||||
| fungible_token_packet | amount | {amount} |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -48,5 +48,6 @@ func GetPrefixedCoin(portID, channelID string, coin sdk.Coin) sdk.Coin {
|
||||
// GetTransferCoin creates a transfer coin with the port ID and channel ID
|
||||
// prefixed to the base denom.
|
||||
func GetTransferCoin(portID, channelID, baseDenom string, amount int64) sdk.Coin {
|
||||
return sdk.NewInt64Coin(GetPrefixedDenom(portID, channelID, baseDenom), amount)
|
||||
denomTrace := ParseDenomTrace(GetPrefixedDenom(portID, channelID, baseDenom))
|
||||
return sdk.NewInt64Coin(denomTrace.IBCDenom(), amount)
|
||||
}
|
||||
|
||||
@ -10,4 +10,5 @@ var (
|
||||
ErrInvalidDenomForTransfer = sdkerrors.Register(ModuleName, 3, "invalid denomination for cross-chain transfer")
|
||||
ErrInvalidVersion = sdkerrors.Register(ModuleName, 4, "invalid ICS20 version")
|
||||
ErrInvalidAmount = sdkerrors.Register(ModuleName, 5, "invalid token amount")
|
||||
ErrTraceNotFound = sdkerrors.Register(ModuleName, 6, "denomination trace not found")
|
||||
)
|
||||
|
||||
@ -6,6 +6,7 @@ const (
|
||||
EventTypePacket = "fungible_token_packet"
|
||||
EventTypeTransfer = "ibc_transfer"
|
||||
EventTypeChannelClose = "channel_closed"
|
||||
EventTypeDenomTrace = "denomination_trace"
|
||||
|
||||
AttributeKeyReceiver = "receiver"
|
||||
AttributeKeyDenom = "denom"
|
||||
@ -15,4 +16,5 @@ const (
|
||||
AttributeKeyRefundAmount = "refund_amount"
|
||||
AttributeKeyAckSuccess = "success"
|
||||
AttributeKeyAckError = "error"
|
||||
AttributeKeyTraceHash = "trace_hash"
|
||||
)
|
||||
|
||||
@ -4,15 +4,27 @@ import (
|
||||
host "github.com/cosmos/cosmos-sdk/x/ibc/24-host"
|
||||
)
|
||||
|
||||
// NewGenesisState creates a new ibc-transfer GenesisState instance.
|
||||
func NewGenesisState(portID string, denomTraces Traces) *GenesisState {
|
||||
return &GenesisState{
|
||||
PortId: portID,
|
||||
DenomTraces: denomTraces,
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultGenesisState returns a GenesisState with "transfer" as the default PortID.
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{
|
||||
PortId: PortID,
|
||||
PortId: PortID,
|
||||
DenomTraces: Traces{},
|
||||
}
|
||||
}
|
||||
|
||||
// Validate performs basic genesis state validation returning an error upon any
|
||||
// failure.
|
||||
func (gs GenesisState) Validate() error {
|
||||
return host.PortIdentifierValidator(gs.PortId)
|
||||
if err := host.PortIdentifierValidator(gs.PortId); err != nil {
|
||||
return err
|
||||
}
|
||||
return gs.DenomTraces.Validate()
|
||||
}
|
||||
|
||||
@ -23,10 +23,10 @@ var _ = math.Inf
|
||||
// proto package needs to be updated.
|
||||
const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
|
||||
|
||||
// GenesisState is currently only used to ensure that the InitGenesis gets run
|
||||
// by the module manager
|
||||
// GenesisState defines the ibc-transfer genesis state
|
||||
type GenesisState struct {
|
||||
PortId string `protobuf:"bytes,1,opt,name=port_id,json=portId,proto3" json:"port_id,omitempty" yaml:"port_id"`
|
||||
PortId string `protobuf:"bytes,1,opt,name=port_id,json=portId,proto3" json:"port_id,omitempty" yaml:"port_id"`
|
||||
DenomTraces Traces `protobuf:"bytes,2,rep,name=denom_traces,json=denomTraces,proto3,castrepeated=Traces" json:"denom_traces" yaml:"denom_traces"`
|
||||
}
|
||||
|
||||
func (m *GenesisState) Reset() { *m = GenesisState{} }
|
||||
@ -69,6 +69,13 @@ func (m *GenesisState) GetPortId() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *GenesisState) GetDenomTraces() Traces {
|
||||
if m != nil {
|
||||
return m.DenomTraces
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*GenesisState)(nil), "ibc.transfer.GenesisState")
|
||||
}
|
||||
@ -76,20 +83,24 @@ func init() {
|
||||
func init() { proto.RegisterFile("ibc/transfer/genesis.proto", fileDescriptor_c13b8463155e05c2) }
|
||||
|
||||
var fileDescriptor_c13b8463155e05c2 = []byte{
|
||||
// 194 bytes of a gzipped FileDescriptorProto
|
||||
// 265 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0xca, 0x4c, 0x4a, 0xd6,
|
||||
0x2f, 0x29, 0x4a, 0xcc, 0x2b, 0x4e, 0x4b, 0x2d, 0xd2, 0x4f, 0x4f, 0xcd, 0x4b, 0x2d, 0xce, 0x2c,
|
||||
0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0xc9, 0x4c, 0x4a, 0xd6, 0x83, 0xc9, 0x49, 0x89,
|
||||
0xa4, 0xe7, 0xa7, 0xe7, 0x83, 0x25, 0xf4, 0x41, 0x2c, 0x88, 0x1a, 0x25, 0x6b, 0x2e, 0x1e, 0x77,
|
||||
0x88, 0xa6, 0xe0, 0x92, 0xc4, 0x92, 0x54, 0x21, 0x6d, 0x2e, 0xf6, 0x82, 0xfc, 0xa2, 0x92, 0xf8,
|
||||
0xcc, 0x14, 0x09, 0x46, 0x05, 0x46, 0x0d, 0x4e, 0x27, 0xa1, 0x4f, 0xf7, 0xe4, 0xf9, 0x2a, 0x13,
|
||||
0x73, 0x73, 0xac, 0x94, 0xa0, 0x12, 0x4a, 0x41, 0x6c, 0x20, 0x96, 0x67, 0x8a, 0x93, 0xf7, 0x89,
|
||||
0x47, 0x72, 0x8c, 0x17, 0x1e, 0xc9, 0x31, 0x3e, 0x78, 0x24, 0xc7, 0x38, 0xe1, 0xb1, 0x1c, 0xc3,
|
||||
0x85, 0xc7, 0x72, 0x0c, 0x37, 0x1e, 0xcb, 0x31, 0x44, 0x19, 0xa6, 0x67, 0x96, 0x64, 0x94, 0x26,
|
||||
0xe9, 0x25, 0xe7, 0xe7, 0xea, 0x27, 0xe7, 0x17, 0xe7, 0xe6, 0x17, 0x43, 0x29, 0xdd, 0xe2, 0x94,
|
||||
0x6c, 0xfd, 0x0a, 0xfd, 0xcc, 0xa4, 0x64, 0x5d, 0xb8, 0xab, 0x4b, 0x2a, 0x0b, 0x52, 0x8b, 0x93,
|
||||
0xd8, 0xc0, 0x0e, 0x32, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0x68, 0xf3, 0x81, 0x61, 0xd2, 0x00,
|
||||
0x00, 0x00,
|
||||
0xa4, 0xe7, 0xa7, 0xe7, 0x83, 0x25, 0xf4, 0x41, 0x2c, 0x88, 0x1a, 0x29, 0x69, 0x14, 0xfd, 0x30,
|
||||
0x06, 0x44, 0x52, 0x69, 0x3e, 0x23, 0x17, 0x8f, 0x3b, 0xc4, 0xc8, 0xe0, 0x92, 0xc4, 0x92, 0x54,
|
||||
0x21, 0x6d, 0x2e, 0xf6, 0x82, 0xfc, 0xa2, 0x92, 0xf8, 0xcc, 0x14, 0x09, 0x46, 0x05, 0x46, 0x0d,
|
||||
0x4e, 0x27, 0xa1, 0x4f, 0xf7, 0xe4, 0xf9, 0x2a, 0x13, 0x73, 0x73, 0xac, 0x94, 0xa0, 0x12, 0x4a,
|
||||
0x41, 0x6c, 0x20, 0x96, 0x67, 0x8a, 0x50, 0x12, 0x17, 0x4f, 0x4a, 0x6a, 0x5e, 0x7e, 0x6e, 0x7c,
|
||||
0x49, 0x51, 0x62, 0x72, 0x6a, 0xb1, 0x04, 0x93, 0x02, 0xb3, 0x06, 0xb7, 0x91, 0x84, 0x1e, 0xb2,
|
||||
0xab, 0xf4, 0x5c, 0x40, 0x2a, 0x42, 0x40, 0x0a, 0x9c, 0x54, 0x4f, 0xdc, 0x93, 0x67, 0xf8, 0x74,
|
||||
0x4f, 0x5e, 0x18, 0x62, 0x1e, 0xb2, 0x5e, 0xa5, 0x55, 0xf7, 0xe5, 0xd9, 0xc0, 0xaa, 0x8a, 0x83,
|
||||
0xb8, 0x53, 0xe0, 0x5a, 0x8a, 0x9d, 0xbc, 0x4f, 0x3c, 0x92, 0x63, 0xbc, 0xf0, 0x48, 0x8e, 0xf1,
|
||||
0xc1, 0x23, 0x39, 0xc6, 0x09, 0x8f, 0xe5, 0x18, 0x2e, 0x3c, 0x96, 0x63, 0xb8, 0xf1, 0x58, 0x8e,
|
||||
0x21, 0xca, 0x30, 0x3d, 0xb3, 0x24, 0xa3, 0x34, 0x49, 0x2f, 0x39, 0x3f, 0x57, 0x3f, 0x39, 0xbf,
|
||||
0x38, 0x37, 0xbf, 0x18, 0x4a, 0xe9, 0x16, 0xa7, 0x64, 0xeb, 0x57, 0xe8, 0x67, 0x26, 0x25, 0xeb,
|
||||
0x22, 0xfc, 0x5d, 0x59, 0x90, 0x5a, 0x9c, 0xc4, 0x06, 0xf6, 0xb5, 0x31, 0x20, 0x00, 0x00, 0xff,
|
||||
0xff, 0xcf, 0x4f, 0x0f, 0xb0, 0x54, 0x01, 0x00, 0x00,
|
||||
}
|
||||
|
||||
func (m *GenesisState) Marshal() (dAtA []byte, err error) {
|
||||
@ -112,6 +123,20 @@ func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) {
|
||||
_ = i
|
||||
var l int
|
||||
_ = l
|
||||
if len(m.DenomTraces) > 0 {
|
||||
for iNdEx := len(m.DenomTraces) - 1; iNdEx >= 0; iNdEx-- {
|
||||
{
|
||||
size, err := m.DenomTraces[iNdEx].MarshalToSizedBuffer(dAtA[:i])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
i -= size
|
||||
i = encodeVarintGenesis(dAtA, i, uint64(size))
|
||||
}
|
||||
i--
|
||||
dAtA[i] = 0x12
|
||||
}
|
||||
}
|
||||
if len(m.PortId) > 0 {
|
||||
i -= len(m.PortId)
|
||||
copy(dAtA[i:], m.PortId)
|
||||
@ -143,6 +168,12 @@ func (m *GenesisState) Size() (n int) {
|
||||
if l > 0 {
|
||||
n += 1 + l + sovGenesis(uint64(l))
|
||||
}
|
||||
if len(m.DenomTraces) > 0 {
|
||||
for _, e := range m.DenomTraces {
|
||||
l = e.Size()
|
||||
n += 1 + l + sovGenesis(uint64(l))
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
@ -213,6 +244,40 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error {
|
||||
}
|
||||
m.PortId = string(dAtA[iNdEx:postIndex])
|
||||
iNdEx = postIndex
|
||||
case 2:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field DenomTraces", wireType)
|
||||
}
|
||||
var msglen int
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowGenesis
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
msglen |= int(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if msglen < 0 {
|
||||
return ErrInvalidLengthGenesis
|
||||
}
|
||||
postIndex := iNdEx + msglen
|
||||
if postIndex < 0 {
|
||||
return ErrInvalidLengthGenesis
|
||||
}
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.DenomTraces = append(m.DenomTraces, DenomTrace{})
|
||||
if err := m.DenomTraces[len(m.DenomTraces)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
|
||||
return err
|
||||
}
|
||||
iNdEx = postIndex
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipGenesis(dAtA[iNdEx:])
|
||||
|
||||
@ -27,8 +27,12 @@ const (
|
||||
QuerierRoute = ModuleName
|
||||
)
|
||||
|
||||
// PortKey defines the key to store the port ID in store
|
||||
var PortKey = []byte{0x01}
|
||||
var (
|
||||
// PortKey defines the key to store the port ID in store
|
||||
PortKey = []byte{0x01}
|
||||
// DenomTraceKey defines the key to store the denomination trace info in store
|
||||
DenomTraceKey = []byte{0x02}
|
||||
)
|
||||
|
||||
// GetEscrowAddress returns the escrow address for the specified channel
|
||||
//
|
||||
|
||||
@ -59,12 +59,7 @@ func (msg MsgTransfer) ValidateBasic() error {
|
||||
if msg.Receiver == "" {
|
||||
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "missing recipient address")
|
||||
}
|
||||
|
||||
// sanity check that validate basic on fungible token packet passes
|
||||
// NOTE: this should always pass since validation checks should be the
|
||||
// same. Please open an issue if you encounter an error on this line.
|
||||
packet := NewFungibleTokenPacketData(msg.Token.Denom, msg.Token.Amount.Uint64(), msg.Sender.String(), msg.Receiver)
|
||||
return packet.ValidateBasic()
|
||||
return ValidateIBCDenom(msg.Token.Denom)
|
||||
}
|
||||
|
||||
// GetSignBytes implements sdk.Msg
|
||||
|
||||
@ -28,8 +28,10 @@ var (
|
||||
emptyAddr sdk.AccAddress
|
||||
|
||||
coin = sdk.NewCoin("atom", sdk.NewInt(100))
|
||||
ibcCoin = sdk.NewCoin("ibc/7F1D3FCF4AE79E1554D670D1AD949A9BA4E4A3C76C63093E17E446A46061A7A2", sdk.NewInt(100))
|
||||
invalidIBCCoin = sdk.NewCoin("notibc/7F1D3FCF4AE79E1554D670D1AD949A9BA4E4A3C76C63093E17E446A46061A7A2", sdk.NewInt(100))
|
||||
invalidDenomCoin = sdk.Coin{Denom: "0atom", Amount: sdk.NewInt(100)}
|
||||
negativeCoin = sdk.Coin{Denom: "atoms", Amount: sdk.NewInt(-100)}
|
||||
zeroCoin = sdk.Coin{Denom: "atoms", Amount: sdk.NewInt(0)}
|
||||
)
|
||||
|
||||
// TestMsgTransferRoute tests Route for MsgTransfer
|
||||
@ -53,7 +55,9 @@ func TestMsgTransferValidation(t *testing.T) {
|
||||
msg *MsgTransfer
|
||||
expPass bool
|
||||
}{
|
||||
{"valid msg", NewMsgTransfer(validPort, validChannel, coin, addr1, addr2, 10, 0), true},
|
||||
{"valid msg with base denom", NewMsgTransfer(validPort, validChannel, coin, addr1, addr2, 10, 0), true},
|
||||
{"valid msg with trace hash", NewMsgTransfer(validPort, validChannel, ibcCoin, addr1, addr2, 10, 0), true},
|
||||
{"invalid ibc denom", NewMsgTransfer(validPort, validChannel, invalidIBCCoin, addr1, addr2, 10, 0), false},
|
||||
{"too short port id", NewMsgTransfer(invalidShortPort, validChannel, coin, addr1, addr2, 10, 0), false},
|
||||
{"too long port id", NewMsgTransfer(invalidLongPort, validChannel, coin, addr1, addr2, 10, 0), false},
|
||||
{"port id contains non-alpha", NewMsgTransfer(invalidPort, validChannel, coin, addr1, addr2, 10, 0), false},
|
||||
@ -61,7 +65,7 @@ func TestMsgTransferValidation(t *testing.T) {
|
||||
{"too long channel id", NewMsgTransfer(validPort, invalidLongChannel, coin, addr1, addr2, 10, 0), false},
|
||||
{"channel id contains non-alpha", NewMsgTransfer(validPort, invalidChannel, coin, addr1, addr2, 10, 0), false},
|
||||
{"invalid denom", NewMsgTransfer(validPort, validChannel, invalidDenomCoin, addr1, addr2, 10, 0), false},
|
||||
{"negative coin", NewMsgTransfer(validPort, validChannel, negativeCoin, addr1, addr2, 10, 0), false},
|
||||
{"zero coin", NewMsgTransfer(validPort, validChannel, zeroCoin, addr1, addr2, 10, 0), false},
|
||||
{"missing sender address", NewMsgTransfer(validPort, validChannel, coin, emptyAddr, addr2, 10, 0), false},
|
||||
{"missing recipient address", NewMsgTransfer(validPort, validChannel, coin, addr1, "", 10, 0), false},
|
||||
{"empty coin", NewMsgTransfer(validPort, validChannel, sdk.Coin{}, addr1, addr2, 10, 0), false},
|
||||
|
||||
@ -36,9 +36,6 @@ func NewFungibleTokenPacketData(
|
||||
|
||||
// ValidateBasic is used for validating the token transfer
|
||||
func (ftpd FungibleTokenPacketData) ValidateBasic() error {
|
||||
if strings.TrimSpace(ftpd.Denom) == "" {
|
||||
return sdkerrors.Wrap(ErrInvalidDenomForTransfer, "denom cannot be empty")
|
||||
}
|
||||
if ftpd.Amount == 0 {
|
||||
return sdkerrors.Wrap(ErrInvalidAmount, "amount cannot be 0")
|
||||
}
|
||||
@ -48,7 +45,7 @@ func (ftpd FungibleTokenPacketData) ValidateBasic() error {
|
||||
if strings.TrimSpace(ftpd.Receiver) == "" {
|
||||
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "receiver address cannot be blank")
|
||||
}
|
||||
return nil
|
||||
return ValidatePrefixedDenom(ftpd.Denom)
|
||||
}
|
||||
|
||||
// GetBytes is a helper for serialising
|
||||
|
||||
1081
x/ibc-transfer/types/query.pb.go
Normal file
1081
x/ibc-transfer/types/query.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
264
x/ibc-transfer/types/query.pb.gw.go
Normal file
264
x/ibc-transfer/types/query.pb.gw.go
Normal file
@ -0,0 +1,264 @@
|
||||
// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.
|
||||
// source: ibc/transfer/query.proto
|
||||
|
||||
/*
|
||||
Package types is a reverse proxy.
|
||||
|
||||
It translates gRPC into RESTful JSON APIs.
|
||||
*/
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/golang/protobuf/descriptor"
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/grpc-ecosystem/grpc-gateway/runtime"
|
||||
"github.com/grpc-ecosystem/grpc-gateway/utilities"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/grpclog"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// Suppress "imported and not used" errors
|
||||
var _ codes.Code
|
||||
var _ io.Reader
|
||||
var _ status.Status
|
||||
var _ = runtime.String
|
||||
var _ = utilities.NewDoubleArray
|
||||
var _ = descriptor.ForMessage
|
||||
|
||||
func request_Query_DenomTrace_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq QueryDenomTraceRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
var (
|
||||
val string
|
||||
ok bool
|
||||
err error
|
||||
_ = err
|
||||
)
|
||||
|
||||
val, ok = pathParams["hash"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "hash")
|
||||
}
|
||||
|
||||
protoReq.Hash, err = runtime.String(val)
|
||||
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "hash", err)
|
||||
}
|
||||
|
||||
msg, err := client.DenomTrace(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func local_request_Query_DenomTrace_0(ctx context.Context, marshaler runtime.Marshaler, server QueryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq QueryDenomTraceRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
var (
|
||||
val string
|
||||
ok bool
|
||||
err error
|
||||
_ = err
|
||||
)
|
||||
|
||||
val, ok = pathParams["hash"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "hash")
|
||||
}
|
||||
|
||||
protoReq.Hash, err = runtime.String(val)
|
||||
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "hash", err)
|
||||
}
|
||||
|
||||
msg, err := server.DenomTrace(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
var (
|
||||
filter_Query_DenomTraces_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
|
||||
)
|
||||
|
||||
func request_Query_DenomTraces_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq QueryDenomTracesRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
if err := req.ParseForm(); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Query_DenomTraces_0); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := client.DenomTraces(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func local_request_Query_DenomTraces_0(ctx context.Context, marshaler runtime.Marshaler, server QueryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq QueryDenomTracesRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
if err := req.ParseForm(); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Query_DenomTraces_0); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := server.DenomTraces(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
// RegisterQueryHandlerServer registers the http handlers for service Query to "mux".
|
||||
// UnaryRPC :call QueryServer directly.
|
||||
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
|
||||
// Note that using this registration option will cause many gRPC library features (such as grpc.SendHeader, etc) to stop working. Consider using RegisterQueryHandlerFromEndpoint instead.
|
||||
func RegisterQueryHandlerServer(ctx context.Context, mux *runtime.ServeMux, server QueryServer) error {
|
||||
|
||||
mux.Handle("GET", pattern_Query_DenomTrace_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_Query_DenomTrace_0(rctx, inboundMarshaler, server, req, pathParams)
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Query_DenomTrace_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("GET", pattern_Query_DenomTraces_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_Query_DenomTraces_0(rctx, inboundMarshaler, server, req, pathParams)
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Query_DenomTraces_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterQueryHandlerFromEndpoint is same as RegisterQueryHandler but
|
||||
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
|
||||
func RegisterQueryHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
|
||||
conn, err := grpc.Dial(endpoint, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if cerr := conn.Close(); cerr != nil {
|
||||
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
|
||||
}
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
if cerr := conn.Close(); cerr != nil {
|
||||
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
|
||||
}
|
||||
}()
|
||||
}()
|
||||
|
||||
return RegisterQueryHandler(ctx, mux, conn)
|
||||
}
|
||||
|
||||
// RegisterQueryHandler registers the http handlers for service Query to "mux".
|
||||
// The handlers forward requests to the grpc endpoint over "conn".
|
||||
func RegisterQueryHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
|
||||
return RegisterQueryHandlerClient(ctx, mux, NewQueryClient(conn))
|
||||
}
|
||||
|
||||
// RegisterQueryHandlerClient registers the http handlers for service Query
|
||||
// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "QueryClient".
|
||||
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "QueryClient"
|
||||
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
|
||||
// "QueryClient" to call the correct interceptors.
|
||||
func RegisterQueryHandlerClient(ctx context.Context, mux *runtime.ServeMux, client QueryClient) error {
|
||||
|
||||
mux.Handle("GET", pattern_Query_DenomTrace_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateContext(ctx, mux, req)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_Query_DenomTrace_0(rctx, inboundMarshaler, client, req, pathParams)
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Query_DenomTrace_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("GET", pattern_Query_DenomTraces_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateContext(ctx, mux, req)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_Query_DenomTraces_0(rctx, inboundMarshaler, client, req, pathParams)
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Query_DenomTraces_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
pattern_Query_DenomTrace_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"ibc_transfer", "v1beta1", "denom_traces", "hash"}, "", runtime.AssumeColonVerbOpt(true)))
|
||||
|
||||
pattern_Query_DenomTraces_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"ibc_transfer", "v1beta1", "denom_traces"}, "", runtime.AssumeColonVerbOpt(true)))
|
||||
)
|
||||
|
||||
var (
|
||||
forward_Query_DenomTrace_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Query_DenomTraces_0 = runtime.ForwardResponseMessage
|
||||
)
|
||||
195
x/ibc-transfer/types/trace.go
Normal file
195
x/ibc-transfer/types/trace.go
Normal file
@ -0,0 +1,195 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
fmt "fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/tendermint/tendermint/crypto/tmhash"
|
||||
tmbytes "github.com/tendermint/tendermint/libs/bytes"
|
||||
tmtypes "github.com/tendermint/tendermint/types"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
host "github.com/cosmos/cosmos-sdk/x/ibc/24-host"
|
||||
)
|
||||
|
||||
// ParseDenomTrace parses a string with the ibc prefix (denom trace) and the base denomination
|
||||
// into a DenomTrace type.
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// - "portidone/channelidone/uatom" => DenomTrace{Path: "portidone/channelidone", BaseDenom: "uatom"}
|
||||
// - "uatom" => DenomTrace{Path: "", BaseDenom: "uatom"}
|
||||
func ParseDenomTrace(rawDenom string) DenomTrace {
|
||||
denomSplit := strings.Split(rawDenom, "/")
|
||||
|
||||
if denomSplit[0] == rawDenom {
|
||||
return DenomTrace{
|
||||
Path: "",
|
||||
BaseDenom: rawDenom,
|
||||
}
|
||||
}
|
||||
|
||||
return DenomTrace{
|
||||
Path: strings.Join(denomSplit[:len(denomSplit)-1], "/"),
|
||||
BaseDenom: denomSplit[len(denomSplit)-1],
|
||||
}
|
||||
}
|
||||
|
||||
// Hash returns the hex bytes of the SHA256 hash of the DenomTrace fields using the following formula:
|
||||
//
|
||||
// hash = sha256(tracePath + "/" + baseDenom)
|
||||
func (dt DenomTrace) Hash() tmbytes.HexBytes {
|
||||
return tmhash.Sum([]byte(dt.GetFullDenomPath()))
|
||||
}
|
||||
|
||||
// GetPrefix returns the receiving denomination prefix composed by the trace info and a separator.
|
||||
func (dt DenomTrace) GetPrefix() string {
|
||||
return dt.Path + "/"
|
||||
}
|
||||
|
||||
// IBCDenom a coin denomination for an ICS20 fungible token in the format
|
||||
// 'ibc/{hash(tracePath + baseDenom)}'. If the trace is empty, it will return the base denomination.
|
||||
func (dt DenomTrace) IBCDenom() string {
|
||||
if dt.Path != "" {
|
||||
return fmt.Sprintf("ibc/%s", dt.Hash())
|
||||
}
|
||||
return dt.BaseDenom
|
||||
}
|
||||
|
||||
// GetFullDenomPath returns the full denomination according to the ICS20 specification:
|
||||
// tracePath + "/" + baseDenom
|
||||
func (dt DenomTrace) GetFullDenomPath() string {
|
||||
return dt.GetPrefix() + dt.BaseDenom
|
||||
}
|
||||
|
||||
func validateTraceIdentifiers(identifiers []string) error {
|
||||
if len(identifiers) == 0 || len(identifiers)%2 != 0 {
|
||||
return errors.New("trace info must come in pairs of port and channel identifiers '{portID}/{channelID}'")
|
||||
}
|
||||
|
||||
// validate correctness of port and channel identifiers
|
||||
for i := 0; i < len(identifiers); i += 2 {
|
||||
if err := host.PortIdentifierValidator(identifiers[i]); err != nil {
|
||||
return sdkerrors.Wrapf(err, "invalid port ID at position %d", i)
|
||||
}
|
||||
if err := host.ChannelIdentifierValidator(identifiers[i+1]); err != nil {
|
||||
return sdkerrors.Wrapf(err, "invalid channel ID at position %d", i)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate performs a basic validation of the DenomTrace fields.
|
||||
func (dt DenomTrace) Validate() error {
|
||||
// empty trace is accepted when token lives on the original chain
|
||||
switch {
|
||||
case dt.Path == "" && dt.BaseDenom != "":
|
||||
return nil
|
||||
case strings.TrimSpace(dt.BaseDenom) == "":
|
||||
return fmt.Errorf("base denomination cannot be blank")
|
||||
}
|
||||
|
||||
// NOTE: no base denomination validation
|
||||
|
||||
identifiers := strings.Split(dt.Path, "/")
|
||||
return validateTraceIdentifiers(identifiers)
|
||||
}
|
||||
|
||||
// Traces defines a wrapper type for a slice of DenomTrace.
|
||||
type Traces []DenomTrace
|
||||
|
||||
// Validate performs a basic validation of each denomination trace info.
|
||||
func (t Traces) Validate() error {
|
||||
seenTraces := make(map[string]bool)
|
||||
for i, trace := range t {
|
||||
hash := trace.Hash().String()
|
||||
if seenTraces[hash] {
|
||||
return fmt.Errorf("duplicated denomination trace with hash %s", trace.Hash())
|
||||
}
|
||||
|
||||
if err := trace.Validate(); err != nil {
|
||||
return sdkerrors.Wrapf(err, "failed denom trace %d validation", i)
|
||||
}
|
||||
seenTraces[hash] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ sort.Interface = Traces{}
|
||||
|
||||
// Len implements sort.Interface for Traces
|
||||
func (t Traces) Len() int { return len(t) }
|
||||
|
||||
// Less implements sort.Interface for Traces
|
||||
func (t Traces) Less(i, j int) bool { return t[i].GetFullDenomPath() < t[j].GetFullDenomPath() }
|
||||
|
||||
// Swap implements sort.Interface for Traces
|
||||
func (t Traces) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
|
||||
|
||||
// Sort is a helper function to sort the set of denomination traces in-place
|
||||
func (t Traces) Sort() Traces {
|
||||
sort.Sort(t)
|
||||
return t
|
||||
}
|
||||
|
||||
// ValidatePrefixedDenom checks that the denomination for an IBC fungible token packet denom is correctly prefixed.
|
||||
// The function will return no error if the given string follows one of the two formats:
|
||||
//
|
||||
// - Prefixed denomination: '{portIDN}/{channelIDN}/.../{portID0}/{channelID0}/baseDenom'
|
||||
// - Unprefixed denomination: 'baseDenom'
|
||||
func ValidatePrefixedDenom(denom string) error {
|
||||
denomSplit := strings.Split(denom, "/")
|
||||
if denomSplit[0] == denom && strings.TrimSpace(denom) != "" {
|
||||
// NOTE: no base denomination validation
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.TrimSpace(denomSplit[len(denomSplit)-1]) == "" {
|
||||
return sdkerrors.Wrap(ErrInvalidDenomForTransfer, "base denomination cannot be blank")
|
||||
}
|
||||
|
||||
identifiers := denomSplit[:len(denomSplit)-1]
|
||||
return validateTraceIdentifiers(identifiers)
|
||||
}
|
||||
|
||||
// ValidateIBCDenom validates that the given denomination is either:
|
||||
//
|
||||
// - A valid base denomination (eg: 'uatom')
|
||||
// - A valid fungible token representation (i.e 'ibc/{hash}') per ADR 001 https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/adr-001-coin-source-tracing.md
|
||||
func ValidateIBCDenom(denom string) error {
|
||||
denomSplit := strings.SplitN(denom, "/", 2)
|
||||
|
||||
switch {
|
||||
case strings.TrimSpace(denom) == "",
|
||||
len(denomSplit) == 1 && denomSplit[0] == "ibc",
|
||||
len(denomSplit) == 2 && (denomSplit[0] != "ibc" || strings.TrimSpace(denomSplit[1]) == ""):
|
||||
return sdkerrors.Wrapf(ErrInvalidDenomForTransfer, "denomination should be prefixed with the format 'ibc/{hash(trace + \"/\" + %s)}'", denom)
|
||||
|
||||
case denomSplit[0] == denom && strings.TrimSpace(denom) != "":
|
||||
return sdk.ValidateDenom(denom)
|
||||
}
|
||||
|
||||
if _, err := ParseHexHash(denomSplit[1]); err != nil {
|
||||
return sdkerrors.Wrapf(err, "invalid denom trace hash %s", denomSplit[1])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseHexHash parses a hex hash in string format to bytes and validates its correctness.
|
||||
func ParseHexHash(hexHash string) (tmbytes.HexBytes, error) {
|
||||
hash, err := hex.DecodeString(hexHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tmtypes.ValidateHash(hash); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return hash, nil
|
||||
}
|
||||
150
x/ibc-transfer/types/trace_test.go
Normal file
150
x/ibc-transfer/types/trace_test.go
Normal file
@ -0,0 +1,150 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseDenomTrace(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
denom string
|
||||
expTrace DenomTrace
|
||||
}{
|
||||
{"empty denom", "", DenomTrace{}},
|
||||
{"base denom", "uatom", DenomTrace{BaseDenom: "uatom"}},
|
||||
{"trace info", "transfer/channelToA/uatom", DenomTrace{BaseDenom: "uatom", Path: "transfer/channelToA"}},
|
||||
{"incomplete path", "transfer/uatom", DenomTrace{BaseDenom: "uatom", Path: "transfer"}},
|
||||
{"invalid path (1)", "transfer//uatom", DenomTrace{BaseDenom: "uatom", Path: "transfer/"}},
|
||||
{"invalid path (2)", "transfer/channelToA/uatom/", DenomTrace{BaseDenom: "", Path: "transfer/channelToA/uatom"}},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
trace := ParseDenomTrace(tc.denom)
|
||||
require.Equal(t, tc.expTrace, trace, tc.name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDenomTrace_IBCDenom(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
trace DenomTrace
|
||||
expDenom string
|
||||
}{
|
||||
{"base denom", DenomTrace{BaseDenom: "uatom"}, "uatom"},
|
||||
{"trace info", DenomTrace{BaseDenom: "uatom", Path: "transfer/channelToA"}, "ibc/7F1D3FCF4AE79E1554D670D1AD949A9BA4E4A3C76C63093E17E446A46061A7A2"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
denom := tc.trace.IBCDenom()
|
||||
require.Equal(t, tc.expDenom, denom, tc.name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDenomTrace_Validate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
trace DenomTrace
|
||||
expError bool
|
||||
}{
|
||||
{"base denom only", DenomTrace{BaseDenom: "uatom"}, false},
|
||||
{"empty DenomTrace", DenomTrace{}, true},
|
||||
{"valid single trace info", DenomTrace{BaseDenom: "uatom", Path: "transfer/channelToA"}, false},
|
||||
{"valid multiple trace info", DenomTrace{BaseDenom: "uatom", Path: "transfer/channelToA/transfer/channelToB"}, false},
|
||||
{"single trace identifier", DenomTrace{BaseDenom: "uatom", Path: "transfer"}, true},
|
||||
{"invalid port ID", DenomTrace{BaseDenom: "uatom", Path: "(transfer)/channelToA"}, true},
|
||||
{"invalid channel ID", DenomTrace{BaseDenom: "uatom", Path: "transfer/(channelToA)"}, true},
|
||||
{"empty base denom with trace", DenomTrace{BaseDenom: "", Path: "transfer/channelToA"}, true},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
err := tc.trace.Validate()
|
||||
if tc.expError {
|
||||
require.Error(t, err, tc.name)
|
||||
continue
|
||||
}
|
||||
require.NoError(t, err, tc.name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraces_Validate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
traces Traces
|
||||
expError bool
|
||||
}{
|
||||
{"empty Traces", Traces{}, false},
|
||||
{"valid multiple trace info", Traces{{BaseDenom: "uatom", Path: "transfer/channelToA/transfer/channelToB"}}, false},
|
||||
{
|
||||
"valid multiple trace info",
|
||||
Traces{
|
||||
{BaseDenom: "uatom", Path: "transfer/channelToA/transfer/channelToB"},
|
||||
{BaseDenom: "uatom", Path: "transfer/channelToA/transfer/channelToB"},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{"empty base denom with trace", Traces{{BaseDenom: "", Path: "transfer/channelToA"}}, true},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
err := tc.traces.Validate()
|
||||
if tc.expError {
|
||||
require.Error(t, err, tc.name)
|
||||
continue
|
||||
}
|
||||
require.NoError(t, err, tc.name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePrefixedDenom(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
denom string
|
||||
expError bool
|
||||
}{
|
||||
{"prefixed denom", "transfer/channelToA/uatom", false},
|
||||
{"base denom", "uatom", false},
|
||||
{"empty denom", "", true},
|
||||
{"empty prefix", "/uatom", true},
|
||||
{"empty identifiers", "//uatom", true},
|
||||
{"single trace identifier", "transfer/", true},
|
||||
{"invalid port ID", "(transfer)/channelToA/uatom", true},
|
||||
{"invalid channel ID", "transfer/(channelToA)/uatom", true},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
err := ValidatePrefixedDenom(tc.denom)
|
||||
if tc.expError {
|
||||
require.Error(t, err, tc.name)
|
||||
continue
|
||||
}
|
||||
require.NoError(t, err, tc.name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateIBCDenom(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
denom string
|
||||
expError bool
|
||||
}{
|
||||
{"denom with trace hash", "ibc/7F1D3FCF4AE79E1554D670D1AD949A9BA4E4A3C76C63093E17E446A46061A7A2", false},
|
||||
{"base denom", "uatom", false},
|
||||
{"empty denom", "", true},
|
||||
{"invalid prefixed denom", "transfer/channelToA/uatom", true},
|
||||
{"denom 'ibc'", "ibc", true},
|
||||
{"denom 'ibc/'", "ibc/", true},
|
||||
{"invald prefix", "notibc/7F1D3FCF4AE79E1554D670D1AD949A9BA4E4A3C76C63093E17E446A46061A7A2", true},
|
||||
{"invald hash", "ibc/!@#$!@#", true},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
err := ValidateIBCDenom(tc.denom)
|
||||
if tc.expError {
|
||||
require.Error(t, err, tc.name)
|
||||
continue
|
||||
}
|
||||
require.NoError(t, err, tc.name)
|
||||
}
|
||||
}
|
||||
@ -260,48 +260,108 @@ func (m *FungibleTokenPacketAcknowledgement) GetError() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// DenomTrace contains the base denomination for ICS20 fungible tokens and the source tracing
|
||||
// information path.
|
||||
type DenomTrace struct {
|
||||
// path defines the chain of port/channel identifiers used for tracing the source of the fungible
|
||||
// token.
|
||||
Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"`
|
||||
// base denomination of the relayed fungible token.
|
||||
BaseDenom string `protobuf:"bytes,2,opt,name=base_denom,json=baseDenom,proto3" json:"base_denom,omitempty"`
|
||||
}
|
||||
|
||||
func (m *DenomTrace) Reset() { *m = DenomTrace{} }
|
||||
func (m *DenomTrace) String() string { return proto.CompactTextString(m) }
|
||||
func (*DenomTrace) ProtoMessage() {}
|
||||
func (*DenomTrace) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_08134a70fd29e656, []int{3}
|
||||
}
|
||||
func (m *DenomTrace) XXX_Unmarshal(b []byte) error {
|
||||
return m.Unmarshal(b)
|
||||
}
|
||||
func (m *DenomTrace) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
if deterministic {
|
||||
return xxx_messageInfo_DenomTrace.Marshal(b, m, deterministic)
|
||||
} else {
|
||||
b = b[:cap(b)]
|
||||
n, err := m.MarshalToSizedBuffer(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b[:n], nil
|
||||
}
|
||||
}
|
||||
func (m *DenomTrace) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_DenomTrace.Merge(m, src)
|
||||
}
|
||||
func (m *DenomTrace) XXX_Size() int {
|
||||
return m.Size()
|
||||
}
|
||||
func (m *DenomTrace) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_DenomTrace.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_DenomTrace proto.InternalMessageInfo
|
||||
|
||||
func (m *DenomTrace) GetPath() string {
|
||||
if m != nil {
|
||||
return m.Path
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *DenomTrace) GetBaseDenom() string {
|
||||
if m != nil {
|
||||
return m.BaseDenom
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*MsgTransfer)(nil), "ibc.transfer.MsgTransfer")
|
||||
proto.RegisterType((*FungibleTokenPacketData)(nil), "ibc.transfer.FungibleTokenPacketData")
|
||||
proto.RegisterType((*FungibleTokenPacketAcknowledgement)(nil), "ibc.transfer.FungibleTokenPacketAcknowledgement")
|
||||
proto.RegisterType((*DenomTrace)(nil), "ibc.transfer.DenomTrace")
|
||||
}
|
||||
|
||||
func init() { proto.RegisterFile("ibc/transfer/transfer.proto", fileDescriptor_08134a70fd29e656) }
|
||||
|
||||
var fileDescriptor_08134a70fd29e656 = []byte{
|
||||
// 505 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x53, 0x4f, 0x6f, 0xd3, 0x30,
|
||||
0x1c, 0x6d, 0xe8, 0x9f, 0x0d, 0x77, 0x43, 0x60, 0x8d, 0x91, 0x15, 0x94, 0x56, 0x39, 0xf5, 0xd2,
|
||||
0x44, 0x05, 0x21, 0x24, 0x4e, 0xb4, 0x43, 0x88, 0x09, 0x21, 0x4d, 0x56, 0x4f, 0x5c, 0xa6, 0xc4,
|
||||
0xf9, 0x91, 0x5a, 0x6d, 0xec, 0xca, 0x76, 0x06, 0x13, 0x5f, 0x82, 0xef, 0xc2, 0x97, 0xd8, 0x71,
|
||||
0x47, 0x4e, 0x11, 0x6a, 0xbf, 0x41, 0x8f, 0x9c, 0x50, 0x62, 0xb7, 0xac, 0xd2, 0xb4, 0x93, 0xfd,
|
||||
0xde, 0xfb, 0xf9, 0x97, 0xe7, 0xf7, 0x73, 0xd0, 0x73, 0x16, 0xd3, 0x50, 0xcb, 0x88, 0xab, 0xaf,
|
||||
0x20, 0xb7, 0x9b, 0x60, 0x21, 0x85, 0x16, 0xf8, 0x80, 0xc5, 0x34, 0xd8, 0x70, 0x9d, 0xa3, 0x54,
|
||||
0xa4, 0xa2, 0x12, 0xc2, 0x72, 0x67, 0x6a, 0x3a, 0x1e, 0x15, 0x2a, 0x13, 0x2a, 0x8c, 0x23, 0x05,
|
||||
0xe1, 0xe5, 0x30, 0x06, 0x1d, 0x0d, 0x43, 0x2a, 0x18, 0x37, 0xba, 0xff, 0xab, 0x8e, 0xda, 0x9f,
|
||||
0x55, 0x3a, 0xb1, 0x5d, 0xf0, 0x1b, 0xd4, 0x56, 0x22, 0x97, 0x14, 0x2e, 0x16, 0x42, 0x6a, 0xd7,
|
||||
0xe9, 0x39, 0xfd, 0x87, 0xe3, 0xe3, 0x75, 0xd1, 0xc5, 0x57, 0x51, 0x36, 0x7f, 0xeb, 0xdf, 0x12,
|
||||
0x7d, 0x82, 0x0c, 0x3a, 0x17, 0x52, 0xe3, 0x77, 0xe8, 0x91, 0xd5, 0xe8, 0x34, 0xe2, 0x1c, 0xe6,
|
||||
0xee, 0x83, 0xea, 0xec, 0xc9, 0xba, 0xe8, 0x3e, 0xdd, 0x39, 0x6b, 0x75, 0x9f, 0x1c, 0x1a, 0xe2,
|
||||
0xd4, 0x60, 0xfc, 0x1a, 0x35, 0xb5, 0x98, 0x01, 0x77, 0xeb, 0x3d, 0xa7, 0xdf, 0x7e, 0x79, 0x12,
|
||||
0x18, 0xeb, 0x41, 0x69, 0x3d, 0xb0, 0xd6, 0x83, 0x53, 0xc1, 0xf8, 0xb8, 0x71, 0x5d, 0x74, 0x6b,
|
||||
0xc4, 0x54, 0xe3, 0x33, 0xd4, 0x52, 0xc0, 0x13, 0x90, 0x6e, 0xa3, 0xe7, 0xf4, 0x0f, 0xc6, 0xc3,
|
||||
0xbf, 0x45, 0x77, 0x90, 0x32, 0x3d, 0xcd, 0xe3, 0x80, 0x8a, 0x2c, 0xb4, 0x01, 0x98, 0x65, 0xa0,
|
||||
0x92, 0x59, 0xa8, 0xaf, 0x16, 0xa0, 0x82, 0x11, 0xa5, 0xa3, 0x24, 0x91, 0xa0, 0x14, 0xb1, 0x0d,
|
||||
0x70, 0x07, 0xed, 0x4b, 0xa0, 0xc0, 0x2e, 0x41, 0xba, 0xcd, 0xd2, 0x3d, 0xd9, 0xe2, 0xf2, 0x7e,
|
||||
0x9a, 0x65, 0x20, 0x72, 0x7d, 0x31, 0x05, 0x96, 0x4e, 0xb5, 0xdb, 0xea, 0x39, 0xfd, 0xc6, 0xed,
|
||||
0xfb, 0xed, 0xea, 0x3e, 0x39, 0xb4, 0xc4, 0xc7, 0x0a, 0xe3, 0x33, 0xf4, 0x64, 0x53, 0x51, 0xae,
|
||||
0x4a, 0x47, 0xd9, 0xc2, 0xdd, 0xab, 0x9a, 0xbc, 0x58, 0x17, 0x5d, 0x77, 0xb7, 0xc9, 0xb6, 0xc4,
|
||||
0x27, 0x8f, 0x2d, 0x37, 0xd9, 0x52, 0x3f, 0xd0, 0xb3, 0x0f, 0x39, 0x4f, 0x59, 0x3c, 0x87, 0x49,
|
||||
0x19, 0xc2, 0x79, 0x44, 0x67, 0xa0, 0xdf, 0x47, 0x3a, 0xc2, 0x47, 0xa8, 0x99, 0x00, 0x17, 0x99,
|
||||
0x19, 0x1d, 0x31, 0x00, 0x1f, 0xa3, 0x56, 0x94, 0x89, 0x9c, 0xeb, 0x6a, 0x2a, 0x0d, 0x62, 0x51,
|
||||
0xc9, 0xdb, 0xf0, 0xea, 0x55, 0xf9, 0x5d, 0x49, 0x34, 0x76, 0x93, 0xf0, 0x27, 0xc8, 0xbf, 0xe3,
|
||||
0xe3, 0x23, 0x3a, 0xe3, 0xe2, 0xdb, 0x1c, 0x92, 0x14, 0x32, 0xe0, 0x1a, 0xbb, 0x68, 0x4f, 0xe5,
|
||||
0x94, 0x82, 0x52, 0x95, 0x93, 0x7d, 0xb2, 0x81, 0xa5, 0x43, 0x90, 0x52, 0x48, 0xf3, 0x40, 0x88,
|
||||
0x01, 0xe3, 0x4f, 0xd7, 0x4b, 0xcf, 0xb9, 0x59, 0x7a, 0xce, 0x9f, 0xa5, 0xe7, 0xfc, 0x5c, 0x79,
|
||||
0xb5, 0x9b, 0x95, 0x57, 0xfb, 0xbd, 0xf2, 0x6a, 0x5f, 0x86, 0xf7, 0x0e, 0xf3, 0x7b, 0xc8, 0x62,
|
||||
0x3a, 0xf8, 0xff, 0x8b, 0x94, 0xb3, 0x8d, 0x5b, 0xd5, 0xe3, 0x7e, 0xf5, 0x2f, 0x00, 0x00, 0xff,
|
||||
0xff, 0x0d, 0x50, 0xec, 0xf7, 0x3f, 0x03, 0x00, 0x00,
|
||||
// 541 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x53, 0xc1, 0x6e, 0xd3, 0x4c,
|
||||
0x10, 0x8e, 0xff, 0xba, 0x69, 0xbb, 0x69, 0x7f, 0xc1, 0xaa, 0x14, 0x37, 0x80, 0x13, 0xf9, 0x94,
|
||||
0x4b, 0x6c, 0x05, 0x84, 0x90, 0xb8, 0x40, 0xd2, 0x0a, 0x51, 0x21, 0xa4, 0xca, 0xca, 0x89, 0x4b,
|
||||
0xb4, 0x5e, 0x0f, 0x8e, 0x95, 0x78, 0x37, 0xda, 0x5d, 0x17, 0x2a, 0x5e, 0x82, 0x77, 0xe1, 0x25,
|
||||
0x7a, 0xec, 0x91, 0x53, 0x84, 0x92, 0x37, 0xc8, 0x91, 0x13, 0x5a, 0xef, 0x26, 0x34, 0x52, 0xc5,
|
||||
0xc9, 0xfb, 0x7d, 0xdf, 0xcc, 0xf8, 0x9b, 0xd9, 0x59, 0xf4, 0x24, 0x4f, 0x68, 0xa4, 0x04, 0x61,
|
||||
0xf2, 0x33, 0x88, 0xcd, 0x21, 0x9c, 0x09, 0xae, 0x38, 0x3e, 0xcc, 0x13, 0x1a, 0xae, 0xb9, 0xe6,
|
||||
0x71, 0xc6, 0x33, 0x5e, 0x09, 0x91, 0x3e, 0x99, 0x98, 0xa6, 0x4f, 0xb9, 0x2c, 0xb8, 0x8c, 0x12,
|
||||
0x22, 0x21, 0xba, 0xea, 0x25, 0xa0, 0x48, 0x2f, 0xa2, 0x3c, 0x67, 0x46, 0x0f, 0x7e, 0xec, 0xa0,
|
||||
0xc6, 0x47, 0x99, 0x0d, 0x6d, 0x15, 0xfc, 0x0a, 0x35, 0x24, 0x2f, 0x05, 0x85, 0xd1, 0x8c, 0x0b,
|
||||
0xe5, 0x39, 0x6d, 0xa7, 0x73, 0x30, 0x38, 0x59, 0xcd, 0x5b, 0xf8, 0x9a, 0x14, 0xd3, 0xd7, 0xc1,
|
||||
0x1d, 0x31, 0x88, 0x91, 0x41, 0x97, 0x5c, 0x28, 0xfc, 0x16, 0xfd, 0x6f, 0x35, 0x3a, 0x26, 0x8c,
|
||||
0xc1, 0xd4, 0xfb, 0xaf, 0xca, 0x3d, 0x5d, 0xcd, 0x5b, 0x8f, 0xb6, 0x72, 0xad, 0x1e, 0xc4, 0x47,
|
||||
0x86, 0x38, 0x33, 0x18, 0xbf, 0x44, 0xbb, 0x8a, 0x4f, 0x80, 0x79, 0x3b, 0x6d, 0xa7, 0xd3, 0x78,
|
||||
0x7e, 0x1a, 0x1a, 0xeb, 0xa1, 0xb6, 0x1e, 0x5a, 0xeb, 0xe1, 0x19, 0xcf, 0xd9, 0xc0, 0xbd, 0x99,
|
||||
0xb7, 0x6a, 0xb1, 0x89, 0xc6, 0x17, 0xa8, 0x2e, 0x81, 0xa5, 0x20, 0x3c, 0xb7, 0xed, 0x74, 0x0e,
|
||||
0x07, 0xbd, 0xdf, 0xf3, 0x56, 0x37, 0xcb, 0xd5, 0xb8, 0x4c, 0x42, 0xca, 0x8b, 0xc8, 0x0e, 0xc0,
|
||||
0x7c, 0xba, 0x32, 0x9d, 0x44, 0xea, 0x7a, 0x06, 0x32, 0xec, 0x53, 0xda, 0x4f, 0x53, 0x01, 0x52,
|
||||
0xc6, 0xb6, 0x00, 0x6e, 0xa2, 0x7d, 0x01, 0x14, 0xf2, 0x2b, 0x10, 0xde, 0xae, 0x76, 0x1f, 0x6f,
|
||||
0xb0, 0xee, 0x4f, 0xe5, 0x05, 0xf0, 0x52, 0x8d, 0xc6, 0x90, 0x67, 0x63, 0xe5, 0xd5, 0xdb, 0x4e,
|
||||
0xc7, 0xbd, 0xdb, 0xdf, 0xb6, 0x1e, 0xc4, 0x47, 0x96, 0x78, 0x5f, 0x61, 0x7c, 0x81, 0x1e, 0xae,
|
||||
0x23, 0xf4, 0x57, 0x2a, 0x52, 0xcc, 0xbc, 0xbd, 0xaa, 0xc8, 0xd3, 0xd5, 0xbc, 0xe5, 0x6d, 0x17,
|
||||
0xd9, 0x84, 0x04, 0xf1, 0x03, 0xcb, 0x0d, 0x37, 0xd4, 0x37, 0xf4, 0xf8, 0x5d, 0xc9, 0xb2, 0x3c,
|
||||
0x99, 0xc2, 0x50, 0x0f, 0xe1, 0x92, 0xd0, 0x09, 0xa8, 0x73, 0xa2, 0x08, 0x3e, 0x46, 0xbb, 0x29,
|
||||
0x30, 0x5e, 0x98, 0xab, 0x8b, 0x0d, 0xc0, 0x27, 0xa8, 0x4e, 0x0a, 0x5e, 0x32, 0x55, 0xdd, 0x8a,
|
||||
0x1b, 0x5b, 0xa4, 0x79, 0x3b, 0xbc, 0x9d, 0x2a, 0xfc, 0xbe, 0x49, 0xb8, 0xdb, 0x93, 0x08, 0x86,
|
||||
0x28, 0xb8, 0xe7, 0xe7, 0x7d, 0x3a, 0x61, 0xfc, 0xcb, 0x14, 0xd2, 0x0c, 0x0a, 0x60, 0x0a, 0x7b,
|
||||
0x68, 0x4f, 0x96, 0x94, 0x82, 0x94, 0x95, 0x93, 0xfd, 0x78, 0x0d, 0xb5, 0x43, 0x10, 0x82, 0x0b,
|
||||
0xb3, 0x20, 0xb1, 0x01, 0xc1, 0x1b, 0x84, 0xce, 0xb5, 0xd5, 0xa1, 0x20, 0x14, 0x30, 0x46, 0xee,
|
||||
0x8c, 0xa8, 0xb1, 0x6d, 0xa2, 0x3a, 0xe3, 0x67, 0x08, 0xe9, 0x55, 0x18, 0x99, 0xf6, 0x4c, 0xf2,
|
||||
0x81, 0x66, 0xaa, 0xbc, 0xc1, 0x87, 0x9b, 0x85, 0xef, 0xdc, 0x2e, 0x7c, 0xe7, 0xd7, 0xc2, 0x77,
|
||||
0xbe, 0x2f, 0xfd, 0xda, 0xed, 0xd2, 0xaf, 0xfd, 0x5c, 0xfa, 0xb5, 0x4f, 0xbd, 0x7f, 0x6e, 0xc3,
|
||||
0xd7, 0x28, 0x4f, 0x68, 0xf7, 0xef, 0x1b, 0xd3, 0xcb, 0x91, 0xd4, 0xab, 0xd7, 0xf1, 0xe2, 0x4f,
|
||||
0x00, 0x00, 0x00, 0xff, 0xff, 0x8b, 0x58, 0xed, 0x2d, 0x80, 0x03, 0x00, 0x00,
|
||||
}
|
||||
|
||||
func (m *MsgTransfer) Marshal() (dAtA []byte, err error) {
|
||||
@ -464,6 +524,43 @@ func (m *FungibleTokenPacketAcknowledgement) MarshalToSizedBuffer(dAtA []byte) (
|
||||
return len(dAtA) - i, nil
|
||||
}
|
||||
|
||||
func (m *DenomTrace) Marshal() (dAtA []byte, err error) {
|
||||
size := m.Size()
|
||||
dAtA = make([]byte, size)
|
||||
n, err := m.MarshalToSizedBuffer(dAtA[:size])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dAtA[:n], nil
|
||||
}
|
||||
|
||||
func (m *DenomTrace) MarshalTo(dAtA []byte) (int, error) {
|
||||
size := m.Size()
|
||||
return m.MarshalToSizedBuffer(dAtA[:size])
|
||||
}
|
||||
|
||||
func (m *DenomTrace) MarshalToSizedBuffer(dAtA []byte) (int, error) {
|
||||
i := len(dAtA)
|
||||
_ = i
|
||||
var l int
|
||||
_ = l
|
||||
if len(m.BaseDenom) > 0 {
|
||||
i -= len(m.BaseDenom)
|
||||
copy(dAtA[i:], m.BaseDenom)
|
||||
i = encodeVarintTransfer(dAtA, i, uint64(len(m.BaseDenom)))
|
||||
i--
|
||||
dAtA[i] = 0x12
|
||||
}
|
||||
if len(m.Path) > 0 {
|
||||
i -= len(m.Path)
|
||||
copy(dAtA[i:], m.Path)
|
||||
i = encodeVarintTransfer(dAtA, i, uint64(len(m.Path)))
|
||||
i--
|
||||
dAtA[i] = 0xa
|
||||
}
|
||||
return len(dAtA) - i, nil
|
||||
}
|
||||
|
||||
func encodeVarintTransfer(dAtA []byte, offset int, v uint64) int {
|
||||
offset -= sovTransfer(v)
|
||||
base := offset
|
||||
@ -548,6 +645,23 @@ func (m *FungibleTokenPacketAcknowledgement) Size() (n int) {
|
||||
return n
|
||||
}
|
||||
|
||||
func (m *DenomTrace) Size() (n int) {
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
var l int
|
||||
_ = l
|
||||
l = len(m.Path)
|
||||
if l > 0 {
|
||||
n += 1 + l + sovTransfer(uint64(l))
|
||||
}
|
||||
l = len(m.BaseDenom)
|
||||
if l > 0 {
|
||||
n += 1 + l + sovTransfer(uint64(l))
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func sovTransfer(x uint64) (n int) {
|
||||
return (math_bits.Len64(x|1) + 6) / 7
|
||||
}
|
||||
@ -1081,6 +1195,123 @@ func (m *FungibleTokenPacketAcknowledgement) Unmarshal(dAtA []byte) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (m *DenomTrace) Unmarshal(dAtA []byte) error {
|
||||
l := len(dAtA)
|
||||
iNdEx := 0
|
||||
for iNdEx < l {
|
||||
preIndex := iNdEx
|
||||
var wire uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowTransfer
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
wire |= uint64(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
fieldNum := int32(wire >> 3)
|
||||
wireType := int(wire & 0x7)
|
||||
if wireType == 4 {
|
||||
return fmt.Errorf("proto: DenomTrace: wiretype end group for non-group")
|
||||
}
|
||||
if fieldNum <= 0 {
|
||||
return fmt.Errorf("proto: DenomTrace: illegal tag %d (wire type %d)", fieldNum, wire)
|
||||
}
|
||||
switch fieldNum {
|
||||
case 1:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field Path", wireType)
|
||||
}
|
||||
var stringLen uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowTransfer
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
stringLen |= uint64(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
intStringLen := int(stringLen)
|
||||
if intStringLen < 0 {
|
||||
return ErrInvalidLengthTransfer
|
||||
}
|
||||
postIndex := iNdEx + intStringLen
|
||||
if postIndex < 0 {
|
||||
return ErrInvalidLengthTransfer
|
||||
}
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.Path = string(dAtA[iNdEx:postIndex])
|
||||
iNdEx = postIndex
|
||||
case 2:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field BaseDenom", wireType)
|
||||
}
|
||||
var stringLen uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowTransfer
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
stringLen |= uint64(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
intStringLen := int(stringLen)
|
||||
if intStringLen < 0 {
|
||||
return ErrInvalidLengthTransfer
|
||||
}
|
||||
postIndex := iNdEx + intStringLen
|
||||
if postIndex < 0 {
|
||||
return ErrInvalidLengthTransfer
|
||||
}
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.BaseDenom = string(dAtA[iNdEx:postIndex])
|
||||
iNdEx = postIndex
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipTransfer(dAtA[iNdEx:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if skippy < 0 {
|
||||
return ErrInvalidLengthTransfer
|
||||
}
|
||||
if (iNdEx + skippy) < 0 {
|
||||
return ErrInvalidLengthTransfer
|
||||
}
|
||||
if (iNdEx + skippy) > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
iNdEx += skippy
|
||||
}
|
||||
}
|
||||
|
||||
if iNdEx > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func skipTransfer(dAtA []byte) (n int, err error) {
|
||||
l := len(dAtA)
|
||||
iNdEx := 0
|
||||
|
||||
@ -7,7 +7,7 @@ order: 1
|
||||
> NOTE: if you are not familiar with the IBC terminology and concepts, please read
|
||||
this [document](https://github.com/cosmos/ics/blob/master/ibc/1_IBC_TERMINOLOGY.md) as prerequisite reading.
|
||||
|
||||
### Connection Version Negotation
|
||||
## Connection Version Negotation
|
||||
|
||||
During the handshake procedure for connections a version string is agreed
|
||||
upon between the two parties. This occurs during the first 3 steps of the
|
||||
@ -33,7 +33,7 @@ A valid connection version is considered to be in the following format:
|
||||
|
||||
- the version tuple must be enclosed in parentheses
|
||||
- the feature set must be enclosed in brackets
|
||||
- there should be no space between the comma separting the identifier and the
|
||||
- there should be no space between the comma separating the identifier and the
|
||||
feature set
|
||||
- the version identifier must no contain any commas
|
||||
- each feature must not contain any commas
|
||||
@ -46,7 +46,7 @@ with regards to version selection in `ConnOpenTry`. Each version in a set of
|
||||
versions should have a unique version identifier.
|
||||
:::
|
||||
|
||||
### Channel Version Negotation
|
||||
## Channel Version Negotation
|
||||
|
||||
During the channel handshake procedure a version must be agreed upon between
|
||||
the two parties. The selection process is largely left to the callers and
|
||||
|
||||
@ -55,6 +55,8 @@ which call each ICS submodule's handlers (i.e `x/ibc/{XX-ICS}/handler.go`).
|
||||
|
||||
The following ADR provide the design and architecture decision of IBC-related components.
|
||||
|
||||
* [ADR 001 - Coin Source Tracing](../../../docs/architecture/adr-001-coin-source-tracing.md): standard to hash the ICS20's fungible token
|
||||
denomination trace path in order to support special characters and limit the maximum denomination length.
|
||||
* [ADR 17 - Historical Header Module](../../../docs/architecture/adr-017-historical-header-module.md): Introduces the ability to introspect past
|
||||
consensus states in order to verify their membership in the counterparty clients.
|
||||
* [ADR 19 - Protobuf State Encoding](../../../docs/architecture/adr-019-protobuf-state-encoding.md): Migration from Amino to Protobuf for state encoding.
|
||||
|
||||
@ -50,7 +50,7 @@ const (
|
||||
// Default params variables used to create a TM client
|
||||
var (
|
||||
DefaultTrustLevel ibctmtypes.Fraction = ibctmtypes.DefaultTrustLevel
|
||||
TestHash = []byte("TESTING HASH")
|
||||
TestHash = tmhash.Sum([]byte("TESTING HASH"))
|
||||
TestCoin = sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100))
|
||||
|
||||
ConnectionVersion = connectiontypes.GetCompatibleEncodedVersions()[0]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user