Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 71 additions & 71 deletions api/api.gen.go

Large diffs are not rendered by default.

28 changes: 23 additions & 5 deletions api/spec/patch.jq
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ def is_rfc3339_field($def; $name):
# delegate_observations.DelegateObservationDoc render it as a string but
# the server sends a number. (operations and governor responses
# really do send it as a string.)
| .definitions["vaa.VaaDoc"].properties.sequence = {type: "integer", format: "int64"}
| .definitions["observations.ObservationDoc"].properties.sequence = {type: "integer", format: "int64"}
| .definitions["delegate_observations.DelegateObservationDoc"].properties.sequence = {type: "integer", format: "int64"}
| .definitions["vaa.VaaDoc"].properties.sequence = {type: "integer", format: "uint64"}
| .definitions["observations.ObservationDoc"].properties.sequence = {type: "integer", format: "uint64"}
| .definitions["delegate_observations.DelegateObservationDoc"].properties.sequence = {type: "integer", format: "uint64"}

# 8. /api/v1/vaas/{chain_id}/{emitter}/{seq} returns {"data": VaaDoc}, not an
# array envelope.
Expand Down Expand Up @@ -162,8 +162,8 @@ def is_rfc3339_field($def; $name):
# typed as strings; the server sends JSON numbers.
# Evidence: GET /api/v1/governor/enqueued_vaas/?pageSize=2 and
# GET /api/v1/governor/enqueued_vaas/1?pageSize=2 on 2026-09-29.
| .definitions["governor.EnqueuedVaa"].properties.sequence = {type: "integer", format: "int64"}
| .definitions["governor.EnqueuedVaaDetail"].properties.sequence = {type: "integer", format: "int64"}
| .definitions["governor.EnqueuedVaa"].properties.sequence = {type: "integer", format: "uint64"}
| .definitions["governor.EnqueuedVaaDetail"].properties.sequence = {type: "integer", format: "uint64"}

# 32. governor.GovConfigfTokens.price is a JSON number; without format:double
# oapi-codegen emits float32 and live prices such as 0.01231905 do not
Expand Down Expand Up @@ -208,3 +208,21 @@ def is_rfc3339_field($def; $name):
# returns *ObservationDoc; the controller JSON-encodes that single value.
| .paths["/api/v1/observations/{chain}/{emitter}/{sequence}/{signer}/{hash}"].get.responses["200"].schema =
{"$ref": "#/definitions/observations.ObservationDoc"}

# 42. Sequences are uint64 and the spec gives integers no format, so the
# generated client takes and decodes them as int/int64. Governance
# sequences are random uint64, so many exceed 2^63-1: the path rejects
# them and a JSON number such as 18220114619187442754 does not decode.
# Every integer `sequence` property and every `seq`/`sequence` path
# parameter is uint64 (rules 7 and 31 already say so for theirs).
# Evidence: GET /api/v1/vaas/1/0000000000000000000000000000000000000000000000000000000000000004/18220114619187442754
# and /api/v1/observations/… with the same id answer 200 with
# "sequence":18220114619187442754 on 2026-10-07.
| .definitions |= map_values(
if .properties.sequence.type? == "integer" then .properties.sequence.format = "uint64" else . end
)
| .paths |= map_values(map_values(
if (type == "object") and has("parameters") then .parameters |= map(
if .in == "path" and (.name == "seq" or .name == "sequence") then .format = "uint64" else . end
) else . end
))
4 changes: 2 additions & 2 deletions governor_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ func fromAPIEnqueuedVaa(src api.GovernorEnqueuedVaa) EnqueuedVAA {
Chain: chainIDFromAPI(src.ChainId),
Emitter: EmitterAddress(deref(src.EmitterAddress)),
NotionalValue: uint64FromInt(src.NotionalValue),
Sequence: uint64FromInt64(deref(src.Sequence)),
Sequence: deref(src.Sequence),
TxHash: TxHash(deref(src.TxHash)),
}
}
Expand All @@ -458,7 +458,7 @@ func fromAPIEnqueuedVaaDetail(src api.GovernorEnqueuedVaaDetail) EnqueuedVAA {
Chain: chainIDFromAPI(src.ChainId),
Emitter: EmitterAddress(deref(src.EmitterAddress)),
NotionalValue: uint64FromInt(src.NotionalValue),
Sequence: uint64FromInt64(deref(src.Sequence)),
Sequence: deref(src.Sequence),
TxHash: TxHash(deref(src.TxHash)),
ReleaseTime: unixTime(src.ReleaseTime),
}
Expand Down
10 changes: 1 addition & 9 deletions guardian/governor.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ func enqueuedVAAsFromAPI(resp *api.GovernorEnqueuedVaaResponse) []GovernorEnqueu
EmitterChain: chainIDFromVAA(item.EmitterChain),
NotionalValue: deref(item.NotionalValue),
ReleaseTime: timeFromUnixSeconds(item.ReleaseTime),
Sequence: uint64FromInt(item.Sequence),
Sequence: deref(item.Sequence),
TxHash: wormholescan.TxHash(deref(item.TxHash)),
})
}
Expand Down Expand Up @@ -175,11 +175,3 @@ func timeFromUnixSeconds(v *int) time.Time {
}
return time.Unix(int64(*v), 0).UTC()
}

// uint64FromInt converts a generated integer pointer to uint64.
func uint64FromInt(v *int) uint64 {
if v == nil || *v < 0 {
return 0
}
return uint64(*v)
}
1 change: 1 addition & 0 deletions guardian/testdata/signed_vaa_governance.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"vaaBytes":"AQAAAAQNAK2jy8xT7Jq+9kgi6zv2F4ShzzbdeXWm8fp5Pf01zVuGWwuNr25+7DCdhwQjxaW3iYzVaTPKgsgklB2TJ2agFKcAAbdzH/ggrVs/l5ckKKgcjIscVaQuiuVfRqle50I/1AdLPwPGCc//kmoJb+ZCock515RpcuG+orzgK0C3ZutbI+EBAyFzj4+2hlLxu0hbFxutw7DTR608vNyVqtARkCCl8dRtHZ2S/UI6NFsPKrKnIqJcrOlqzKDRxPLIOShe0GpvD7IBBOZ87fHUglGBesy3Nkew0W4dVl0ZUbr2KxZUsLsm1RaZLfEx3978xJmkG3pJ3q0MsDSY72EIAY2Iq1yAOXqDQyQBBX9qBubRD11s2EtVLoZqvFjJiZuypolW6q/KWYjBno/JeaHVMGNvH4l/sDnRCNC3/U/ehnn3UkTPPhD/0fYFGQwBBnXaxX2ZjYjqgK1weLa/iNc6vo7SypYKF3Nsmn2/oWj6HIQ7fkOIOgoV0IhL8qOXrKzvFC2lHuwBusMxvW466xYACDkdwLnu0/rcV1KPLDKmoGCDOs7W1he5XTnqADAMo+FZUbF10dp0HTEpPTu+3Rgm1whnX+s7zlbFhTVZLN1KVZIACo8wvecRwtK35/RTvnpCuNpyQAijRZfDKcOYns3U82j1DWG3uMTH2fRuKDNMQYXQMoTZBVKx+YzwzYLGP7Xsq2MADPZ/tOBUY8OYKiN50fd5rUNkumylskDegq9K/8TG7lilKpcuIbuyVPFaTqdMtRHjLwbAIdnndLNFJuaB36NTdZMADqE8945xnHI4/OsW9voesI3kxRaz5MSRtjOC/fPfUZlUMYPBymdiUXj7NBSE9YoYN5utc2JWN/C2S65rZ2uJAz0AD45M+5XQH0+EvySQOPZMEPstlPOgQpsB+FNmXe7r6uXkfp2TkmyX4ZJbS+Nz5u+Wpga8AysnobvO+VyqXE8k3qABERGp/FbgkPsBpzlhHKn9PeQOxT7uuDPitPurLf3BdtYMc0UyJI8rLl4PvuhAgkWoyl1XRAxtWW6YGt51Cm4XkooBEpbi/UGXFBOUmH0GIY97mbcJrfueBRGhd/m/3o5/vh1LVbnkwq86cNL3PW3xLDpg5qZ31Hb/Q5O5d58nNdvNNugBabau5KScIFsAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE/NrZtjuRtEIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAENvcmUCAAAAAAAFE1iTtadsP3OWRWSIhb3MwGzXCjzT/2y5Ulib3oYsJe9DkhMvudSkIVcRTehGAZO986L8+B+GoJdl9HYv0RB6AIazLXoJd5JqIFEx2HMdOcvrjIKy/YL67ScR1Zrw8kmdFucm9rIRs5dWwEJEG+bYZQtptU6+cV4jQ5OPEErrVYEpMhbOl9dx4MtyEiGxFefK8HxOPcjnxGn5LIzYj7gAWiB0o7+ROVPWlSYNiLwaolpO7jY+8AAKwAdnJ7NfvqLawo/uXMsP6naOr0XO0Ta52eJJA0ZK6In1yKcj/BT5MSS3xziEPLuJ6GTIYsOM3czPldLMN6TcA2qNIytI9izdRzFBL0iQ2nmPaJajMx9ktIwS0dV/2cvnCBHR9k4mI4gR3lVTxA9kr0HuG2BXzEOsj1Z6MeeFDaUys2GYi/4NOuEbF44hrS53rgZxFUnPux+cep2AluheFIfzVRXQKpJ1NQSo11RxufSe22++vImPQD5Hc+lf6xXoDJqZyDSN"}
5 changes: 2 additions & 3 deletions guardian/vaa.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ func (c *Client) GetSignedBatchVAA(ctx context.Context, id wormholescan.VAAID) (
}

// vaaPathParams converts a [wormholescan.VAAID] into generated path parameters.
func vaaPathParams(id wormholescan.VAAID) (int, string, int) {
//nolint:gosec // path params are chain uint16 and sequence that fit int
return int(id.Chain), string(id.Emitter), int(id.Sequence)
func vaaPathParams(id wormholescan.VAAID) (int, string, uint64) {
return int(id.Chain), string(id.Emitter), id.Sequence
}
22 changes: 22 additions & 0 deletions guardian/vaa_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,25 @@ func sampleVAAID() wormholescan.VAAID {
Sequence: 755119,
}
}

// TestGetSignedVAASequenceAboveMaxInt64 sends a sequence above 2^63-1 as
// its decimal value; the fixture is the live response for it.
func TestGetSignedVAASequenceAboveMaxInt64(t *testing.T) {
t.Parallel()

id := wormholescan.VAAID{
Chain: wormholescan.ChainIDSolana,
Emitter: "0000000000000000000000000000000000000000000000000000000000000004",
Sequence: 18220114619187442754,
}
var path string
c := newClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path = r.URL.Path
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(loadFixture(t, "signed_vaa_governance.json"))
}))
got, err := c.GetSignedVAA(t.Context(), id)
require.NoError(t, err)
assert.Equal(t, "/v1/signed_vaa/1/"+string(id.Emitter)+"/18220114619187442754", path)
assert.NotEmpty(t, got)
}
28 changes: 6 additions & 22 deletions observation.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,15 +204,11 @@ func (c *Client) ListObservationsByVAA(
id VAAID,
opts ObservationListOptions,
) (Page[Observation], error) {
seq, err := pathSequence(id.Sequence)
if err != nil {
return Page[Observation]{}, err
}
rsp, err := c.api.FindObservationsBySequenceWithResponse(
ctx,
pathChain(id.Chain),
string(id.Emitter),
seq,
id.Sequence,
findObservationsBySequenceParams(opts),
)
if err != nil {
Expand Down Expand Up @@ -255,15 +251,11 @@ func (c *Client) GetObservation(
signer GuardianAddress,
hash string,
) (Observation, error) {
seq, err := pathSequence(id.Sequence)
if err != nil {
return Observation{}, err
}
rsp, err := c.api.FindObservationsByIdWithResponse(
ctx,
pathChain(id.Chain),
string(id.Emitter),
seq,
id.Sequence,
string(signer),
hash,
nil,
Expand Down Expand Up @@ -369,15 +361,11 @@ func (c *Client) ListDelegateObservationsByVAA(
id VAAID,
opts ObservationListOptions,
) (Page[DelegateObservation], error) {
seq, err := pathSequence(id.Sequence)
if err != nil {
return Page[DelegateObservation]{}, err
}
rsp, err := c.api.FindDelegateObservationsBySequenceWithResponse(
ctx,
pathChain(id.Chain),
string(id.Emitter),
seq,
id.Sequence,
findDelegateObservationsBySequenceParams(opts),
)
if err != nil {
Expand Down Expand Up @@ -417,15 +405,11 @@ func (c *Client) GetDelegateObservation(
id VAAID,
guardian GuardianAddress,
) (DelegateObservation, error) {
seq, err := pathSequence(id.Sequence)
if err != nil {
return DelegateObservation{}, err
}
rsp, err := c.api.FindDelegateObservationsByGuardianWithResponse(
ctx,
pathChain(id.Chain),
string(id.Emitter),
seq,
id.Sequence,
string(guardian),
nil,
)
Expand Down Expand Up @@ -477,7 +461,7 @@ func observationFromAPI(doc api.ObservationsObservationDoc) Observation {
ID: deref(doc.Id),
EmitterChain: chainFromAPI(deref(doc.EmitterChain)),
EmitterAddress: EmitterAddress(deref(doc.EmitterAddr)),
Sequence: uint64FromInt64(deref(doc.Sequence)),
Sequence: deref(doc.Sequence),
Hash: deref(doc.Hash),
TxHash: deref(doc.TxHash),
GuardianAddress: GuardianAddress(deref(doc.GuardianAddr)),
Expand Down Expand Up @@ -522,7 +506,7 @@ func delegateObservationFromAPI(doc api.DelegateObservationsDelegateObservationD
ID: deref(doc.Id),
EmitterChain: chainFromAPI(deref(doc.EmitterChain)),
EmitterAddress: EmitterAddress(deref(doc.EmitterAddr)),
Sequence: uint64FromInt64(deref(doc.Sequence)),
Sequence: deref(doc.Sequence),
Hash: deref(doc.Hash),
TxHash: deref(doc.TxHash),
DelegatedGuardianAddr: GuardianAddress(deref(doc.DelegatedGuardianAddr)),
Expand Down
36 changes: 3 additions & 33 deletions operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"fmt"
"iter"
"math"
"strconv"
"strings"
"time"
Expand All @@ -29,11 +28,7 @@ const (

// GetOperation fetches one operation by VAA id (get-operation-by-id).
func (c *Client) GetOperation(ctx context.Context, id VAAID) (Operation, error) {
chain, emitter, seq, err := vaaIDPath(id)
if err != nil {
return Operation{}, err
}
rsp, err := c.api.GetOperationByIdWithResponse(ctx, chain, emitter, seq)
rsp, err := c.api.GetOperationByIdWithResponse(ctx, pathChain(id.Chain), string(id.Emitter), id.Sequence)
if err != nil {
return Operation{}, wrapCall(opGetOperationByID, err)
}
Expand Down Expand Up @@ -99,11 +94,7 @@ func (c *Client) SearchOperationsByTxHashes(ctx context.Context, hashes []TxHash

// GetRelay fetches Wormhole Relayer delivery info for a VAA (find-relay-by-vaa-id).
func (c *Client) GetRelay(ctx context.Context, id VAAID) (Relay, error) {
chain, emitter, seq, err := vaaIDPath(id)
if err != nil {
return Relay{}, err
}
rsp, err := c.api.FindRelayByVaaIdWithResponse(ctx, chain, emitter, seq)
rsp, err := c.api.FindRelayByVaaIdWithResponse(ctx, pathChain(id.Chain), string(id.Emitter), id.Sequence)
if err != nil {
return Relay{}, wrapCall(opFindRelay, err)
}
Expand All @@ -119,11 +110,7 @@ func (c *Client) GetRelay(ctx context.Context, id VAAID) (Relay, error) {
// GetGlobalTransaction fetches the origin and destination transactions for a VAA
// (find-global-transaction-by-id). Destination is zero when the VAA is unredeemed.
func (c *Client) GetGlobalTransaction(ctx context.Context, id VAAID) (GlobalTransaction, error) {
chain, emitter, seq, err := vaaIDPath(id)
if err != nil {
return GlobalTransaction{}, err
}
rsp, err := c.api.FindGlobalTransactionByIdWithResponse(ctx, chain, emitter, seq)
rsp, err := c.api.FindGlobalTransactionByIdWithResponse(ctx, pathChain(id.Chain), string(id.Emitter), id.Sequence)
if err != nil {
return GlobalTransaction{}, wrapCall(opFindGlobalTx, err)
}
Expand Down Expand Up @@ -198,23 +185,6 @@ func joinChainIDs(ids []ChainID) string {
return strings.Join(parts, chainListSep)
}

// vaaIDPath converts a VAA id into generated path parameters.
func vaaIDPath(id VAAID) (int, string, int, error) {
seq, err := sequenceParam(id.Sequence)
if err != nil {
return 0, "", 0, err
}
return int(id.Chain), string(id.Emitter), seq, nil
}

// sequenceParam converts a VAA sequence into a generated path parameter.
func sequenceParam(seq uint64) (int, error) {
if seq > uint64(math.MaxInt) {
return 0, fmt.Errorf("%sVAA sequence %d exceeds path parameter range", errPrefix, seq)
}
return int(seq), nil
}

// wrapCall prefixes a generated-client error with the operation id.
func wrapCall(op string, err error) error {
return fmt.Errorf("%s%s: %w", errPrefix, op, err)
Expand Down
54 changes: 54 additions & 0 deletions sequence_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package wormholescan

import (
"math"
"net/http"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// governanceVAAID is the Core upgrade to guardian set 5. Its sequence,
// like many governance sequences, is above [math.MaxInt64].
const governanceVAAID = "1/0000000000000000000000000000000000000000000000000000000000000004/18220114619187442754"

// TestSequenceAboveMaxInt64 sends a sequence above 2^63-1 in the path as
// its decimal value and decodes it from the JSON number the server
// returns. The fixtures are live responses for governanceVAAID.
func TestSequenceAboveMaxInt64(t *testing.T) {
t.Parallel()

id, err := ParseVAAID(governanceVAAID)
require.NoError(t, err)
require.Greater(t, id.Sequence, uint64(math.MaxInt64))

t.Run("GetVAA", func(t *testing.T) {
t.Parallel()
var path string
c := newTestClientNoRetry(t, func(w http.ResponseWriter, r *http.Request) {
path = r.URL.Path
writeFixture(t, w, "vaa_by_id_governance.json")
})
got, err := c.GetVAA(t.Context(), id, VAAGetOptions{})
require.NoError(t, err)
assert.Equal(t, "/api/v1/vaas/"+governanceVAAID, path)
assert.Equal(t, id, got.ID)
})

t.Run("ListObservationsByVAA", func(t *testing.T) {
t.Parallel()
var path string
c := newTestClientNoRetry(t, func(w http.ResponseWriter, r *http.Request) {
path = r.URL.Path
writeFixture(t, w, "observation_list_governance.json")
})
page, err := c.ListObservationsByVAA(t.Context(), id, ObservationListOptions{})
require.NoError(t, err)
assert.Equal(t, "/api/v1/observations/"+governanceVAAID, path)
require.NotEmpty(t, page.Items)
for _, obs := range page.Items {
assert.Equal(t, id.Sequence, obs.Sequence)
}
})
}
Loading
Loading