denom.gno
5.01 Kb · 188 lines
1package transfer
2
3import (
4 "crypto/sha256"
5 "encoding/hex"
6 "strings"
7
8 "gno.land/p/aib/ibc/host"
9 "gno.land/p/aib/ibc/types"
10 "gno.land/p/nt/ufmt/v0"
11 "gno.land/p/onbloc/json"
12)
13
14// Denom holds the base denom of a Token and a trace of the chains it was sent
15// through.
16type Denom struct {
17 // the base token denomination
18 Base string
19 // the trace of the token
20 Trace []Hop
21}
22
23// NewDenom creates a new Denom instance given the base denomination and a
24// variable number of hops.
25func NewDenom(base string, trace ...Hop) Denom {
26 return Denom{
27 Base: base,
28 Trace: trace,
29 }
30}
31
32// ValidateBasic performs a basic validation of the Denom fields.
33func (d Denom) ValidateBasic() error {
34 // NOTE: base denom validation cannot be performed as each chain may define
35 // its own base denom validation
36 if strings.TrimSpace(d.Base) == "" {
37 return ufmt.Errorf("base denomination cannot be blank")
38 }
39
40 for _, hop := range d.Trace {
41 if err := hop.ValidateBasic(); err != nil {
42 return ufmt.Errorf("invalid trace")
43 }
44 }
45
46 return nil
47}
48
49// IsNative returns true if the denomination is native, thus containing no
50// trace history.
51func (d Denom) IsNative() bool {
52 return len(d.Trace) == 0
53}
54
55// Path returns the full denomination according to the ICS20 specification:
56// trace + "/" + baseDenom
57// If there exists no trace then the base denomination is returned.
58func (d Denom) Path() string {
59 if d.IsNative() {
60 return d.Base
61 }
62
63 var sb strings.Builder
64 for _, t := range d.Trace {
65 sb.WriteString(t.String())
66 sb.WriteByte('/')
67 }
68 sb.WriteString(d.Base)
69 return sb.String()
70}
71
72// HasPrefix returns true if the first element of the trace of the denom
73// matches the provided portId and clientId.
74func (d Denom) HasPrefix(portID, clientID string) bool {
75 // if the denom is native, then it is not prefixed by any port/channel pair
76 if d.IsNative() {
77 return false
78 }
79 return d.Trace[0].PortId == portID && d.Trace[0].ClientId == clientID
80}
81
82// Hash returns the hex bytes of the SHA256 hash of the Denom fields using the
83// following formula:
84//
85// hash = sha256(trace + "/" + baseDenom)
86func (d Denom) Hash() []byte {
87 hash := sha256.Sum256([]byte(d.Path()))
88 return hash[:]
89}
90
91func (d Denom) HashHex() string {
92 return strings.ToUpper(hex.EncodeToString(d.Hash()))
93}
94
95// IBCDenom a coin denomination for an ICS20 fungible token in the format
96// 'ibc/{hash(trace + baseDenom)}'. If the trace is empty, it will return the
97// base denomination.
98func (d Denom) IBCDenom() string {
99 if d.IsNative() {
100 return d.Base
101 }
102 return ufmt.Sprintf("%s/%s", denomPrefix, d.HashHex())
103}
104
105// Implements jsonpage.JSONRenderer
106func (d Denom) RenderJSON() *json.Node {
107 return json.ObjectNode("", map[string]*json.Node{
108 "base": json.StringNode("", d.Base),
109 "path": json.StringNode("", d.Path()),
110 "denom": json.StringNode("", d.IBCDenom()),
111 })
112}
113
114func (d Denom) MarshalJSON() []byte {
115 return []byte(d.RenderJSON().String())
116}
117
118// ExtractDenomFromPath returns the denom from the full path.
119func ExtractDenomFromPath(fullPath string) Denom {
120 denomSplit := strings.Split(fullPath, "/")
121
122 if denomSplit[0] == fullPath {
123 return Denom{
124 Base: fullPath,
125 }
126 }
127
128 var (
129 trace []Hop
130 baseDenomSlice []string
131 )
132
133 length := len(denomSplit)
134 for i := 0; i < length; i += 2 {
135 // The IBC specification does not guarantee the expected format of the
136 // destination port or destination channel identifier. A short term
137 // solution to determine base denomination is to expect the channel
138 // identifier to be the one ibc-go specifies. A longer term solution is to
139 // separate the path and base denomination in the ICS20 packet. If an
140 // intermediate hop prefixes the full denom with a channel identifier
141 // format different from our own, the base denomination will be incorrectly
142 // parsed, but the token will continue to be treated correctly as an IBC
143 // denomination. The hash used to store the token internally on our chain
144 // will be the same value as the base denomination being correctly parsed.
145 if i < length-1 && length > 2 && types.IsValidClientID(denomSplit[i+1]) {
146 trace = append(trace, NewHop(denomSplit[i], denomSplit[i+1]))
147 } else {
148 baseDenomSlice = denomSplit[i:]
149 break
150 }
151 }
152
153 base := strings.Join(baseDenomSlice, "/")
154
155 return Denom{
156 Base: base,
157 Trace: trace,
158 }
159}
160
161// Hop defines a port ID, client ID pair specifying a unique "hop" in a trace
162type Hop struct {
163 PortId string
164 ClientId string
165}
166
167// NewHop creates a Hop with the given port ID and client ID.
168func NewHop(portID, clientID string) Hop {
169 return Hop{portID, clientID}
170}
171
172// ValidateBasic performs a basic validation of the Hop fields.
173func (h Hop) ValidateBasic() error {
174 if err := host.PortIdentifierValidator(h.PortId); err != nil {
175 return ufmt.Errorf("invalid hop source port ID %s", h.PortId)
176 }
177 if err := host.ClientIdentifierValidator(h.ClientId); err != nil {
178 return ufmt.Errorf("invalid hop source client ID %s", h.ClientId)
179 }
180
181 return nil
182}
183
184// String returns the Hop in the format:
185// <portID>/<clientID>
186func (h Hop) String() string {
187 return ufmt.Sprintf("%s/%s", h.PortId, h.ClientId)
188}