diff --git a/api/docs/docs.go b/api/docs/docs.go index 79e4eb73..5dd542ba 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -1,5 +1,4 @@ -// Package docs GENERATED BY SWAG; DO NOT EDIT -// This file was generated by swaggo/swag +// Code generated by swaggo/swag. DO NOT EDIT package docs import "github.com/swaggo/swag" @@ -25,6 +24,53 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { + "/api/v1/address/{address}": { + "get": { + "description": "Lookup an address", + "tags": [ + "Wormscan" + ], + "operationId": "find-address-by-id", + "parameters": [ + { + "type": "string", + "description": "address", + "name": "address", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Page number. Starts at 0.", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Number of elements per page.", + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response-address_AddressOverview" + } + }, + "400": { + "description": "Bad Request" + }, + "404": { + "description": "Not Found" + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, "/api/v1/global-tx/{chain_id}/{emitter}/{seq}": { "get": { "description": "Find a global transaction by ID.", @@ -1515,6 +1561,17 @@ const docTemplate = `{ } }, "definitions": { + "address.AddressOverview": { + "type": "object", + "properties": { + "vaas": { + "type": "array", + "items": { + "$ref": "#/definitions/vaa.VaaDoc" + } + } + } + }, "github_com_wormhole-foundation_wormhole-explorer_api_routes_guardian_guardian.GuardianSet": { "type": "object", "properties": { @@ -2046,6 +2103,17 @@ const docTemplate = `{ } } }, + "response.Response-address_AddressOverview": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/address.AddressOverview" + }, + "pagination": { + "$ref": "#/definitions/response.ResponsePagination" + } + } + }, "response.Response-array_governor_EnqueuedVaaDetail": { "type": "object", "properties": { @@ -2356,6 +2424,10 @@ const docTemplate = `{ "indexedAt": { "type": "string" }, + "nativeTxHash": { + "description": "NativeTxHash is an extension field - it is not present in the guardian API.", + "type": "string" + }, "payload": { "description": "Payload is an extension field - it is not present in the guardian API.", "type": "object", diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 6bfd8b4c..f1d7cfd6 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -17,6 +17,53 @@ }, "basePath": "/v1", "paths": { + "/api/v1/address/{address}": { + "get": { + "description": "Lookup an address", + "tags": [ + "Wormscan" + ], + "operationId": "find-address-by-id", + "parameters": [ + { + "type": "string", + "description": "address", + "name": "address", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Page number. Starts at 0.", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Number of elements per page.", + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response-address_AddressOverview" + } + }, + "400": { + "description": "Bad Request" + }, + "404": { + "description": "Not Found" + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, "/api/v1/global-tx/{chain_id}/{emitter}/{seq}": { "get": { "description": "Find a global transaction by ID.", @@ -1507,6 +1554,17 @@ } }, "definitions": { + "address.AddressOverview": { + "type": "object", + "properties": { + "vaas": { + "type": "array", + "items": { + "$ref": "#/definitions/vaa.VaaDoc" + } + } + } + }, "github_com_wormhole-foundation_wormhole-explorer_api_routes_guardian_guardian.GuardianSet": { "type": "object", "properties": { @@ -2038,6 +2096,17 @@ } } }, + "response.Response-address_AddressOverview": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/address.AddressOverview" + }, + "pagination": { + "$ref": "#/definitions/response.ResponsePagination" + } + } + }, "response.Response-array_governor_EnqueuedVaaDetail": { "type": "object", "properties": { @@ -2348,6 +2417,10 @@ "indexedAt": { "type": "string" }, + "nativeTxHash": { + "description": "NativeTxHash is an extension field - it is not present in the guardian API.", + "type": "string" + }, "payload": { "description": "Payload is an extension field - it is not present in the guardian API.", "type": "object", diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index 150b2819..5800aa87 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -1,5 +1,12 @@ basePath: /v1 definitions: + address.AddressOverview: + properties: + vaas: + items: + $ref: '#/definitions/vaa.VaaDoc' + type: array + type: object github_com_wormhole-foundation_wormhole-explorer_api_routes_guardian_guardian.GuardianSet: properties: addresses: @@ -345,6 +352,13 @@ definitions: version: type: integer type: object + response.Response-address_AddressOverview: + properties: + data: + $ref: '#/definitions/address.AddressOverview' + pagination: + $ref: '#/definitions/response.ResponsePagination' + type: object response.Response-array_governor_EnqueuedVaaDetail: properties: data: @@ -566,6 +580,10 @@ definitions: type: string indexedAt: type: string + nativeTxHash: + description: NativeTxHash is an extension field - it is not present in the + guardian API. + type: string payload: additionalProperties: true description: Payload is an extension field - it is not present in the guardian @@ -609,6 +627,37 @@ info: title: Wormhole Guardian API version: "1.0" paths: + /api/v1/address/{address}: + get: + description: Lookup an address + operationId: find-address-by-id + parameters: + - description: address + in: path + name: address + required: true + type: string + - description: Page number. Starts at 0. + in: query + name: page + type: integer + - description: Number of elements per page. + in: query + name: pageSize + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response-address_AddressOverview' + "400": + description: Bad Request + "404": + description: Not Found + "500": + description: Internal Server Error + tags: + - Wormscan /api/v1/global-tx/{chain_id}/{emitter}/{seq}: get: description: Find a global transaction by ID. diff --git a/api/go.mod b/api/go.mod index 2e612097..c596e8d8 100644 --- a/api/go.mod +++ b/api/go.mod @@ -85,7 +85,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect - github.com/mr-tron/base58 v1.2.0 // indirect + github.com/mr-tron/base58 v1.2.0 github.com/multiformats/go-base32 v0.0.4 // indirect github.com/multiformats/go-base36 v0.1.0 // indirect github.com/multiformats/go-multiaddr v0.6.0 // indirect diff --git a/api/handlers/address/model.go b/api/handlers/address/model.go new file mode 100644 index 00000000..f5490542 --- /dev/null +++ b/api/handlers/address/model.go @@ -0,0 +1,7 @@ +package address + +import "github.com/wormhole-foundation/wormhole-explorer/api/handlers/vaa" + +type AddressOverview struct { + Vaas []*vaa.VaaDoc `json:"vaas"` +} diff --git a/api/handlers/address/repository.go b/api/handlers/address/repository.go new file mode 100644 index 00000000..32377c64 --- /dev/null +++ b/api/handlers/address/repository.go @@ -0,0 +1,123 @@ +package address + +import ( + "context" + "fmt" + + "github.com/wormhole-foundation/wormhole-explorer/api/handlers/vaa" + "github.com/wormhole-foundation/wormhole-explorer/api/types" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +type Repository struct { + db *mongo.Database + logger *zap.Logger + + collections struct { + parsedVaa *mongo.Collection + } +} + +func NewRepository(db *mongo.Database, logger *zap.Logger) *Repository { + return &Repository{db: db, + logger: logger.With(zap.String("module", "AddressRepository")), + collections: struct { + parsedVaa *mongo.Collection + }{ + parsedVaa: db.Collection("parsedVaa"), + }, + } +} + +type GetAddressOverviewParams struct { + Address *types.Address + Skip int64 + Limit int64 +} + +func (r *Repository) GetAddressOverview(ctx context.Context, params *GetAddressOverviewParams) (*AddressOverview, error) { + + // build a query pipeline based on input parameters + var pipeline mongo.Pipeline + { + // filter by address + pipeline = append(pipeline, bson.D{ + {"$match", bson.D{ + {"$or", bson.A{ + bson.D{{"result.fromAddress", bson.D{{"$eq", "0x" + params.Address.Hex()}}}}, + bson.D{{"result.toAddress", bson.M{"$eq": "0x" + params.Address.Hex()}}}, + }}, + }}, + }) + + // specify sorting criteria + pipeline = append(pipeline, bson.D{ + {"$sort", bson.D{bson.E{"indexedAt", -1}}}, + }) + + // left outer join on the `vaas` collection + pipeline = append(pipeline, bson.D{ + {"$lookup", bson.D{ + {"from", "vaas"}, + {"localField", "_id"}, + {"foreignField", "_id"}, + {"as", "vaas"}, + }}, + }) + + // skip initial results + if params.Skip != 0 { + pipeline = append(pipeline, bson.D{ + {"$skip", params.Skip}, + }) + } + + // limit size of results + pipeline = append(pipeline, bson.D{ + {"$limit", params.Limit}, + }) + } + + // execute the aggregation pipeline + cur, err := r.collections.parsedVaa.Aggregate(ctx, pipeline) + if err != nil { + requestID := fmt.Sprintf("%v", ctx.Value("requestid")) + r.logger.Error("failed execute Aggregate command to get vaa with payload", + zap.Error(err), + zap.Any("params", params), + zap.String("requestID", requestID), + ) + return nil, err + } + + // read results from cursor + var documents []struct { + ID string `bson:"_id"` + Vaas []vaa.VaaDoc `bson:"vaas"` + } + err = cur.All(ctx, &documents) + if err != nil { + requestID := fmt.Sprintf("%v", ctx.Value("requestid")) + r.logger.Error("failed to decode cursor for account activity", + zap.Error(err), + zap.Any("params", params), + zap.String("requestID", requestID), + ) + return nil, err + } + + // build the result and return + var vaas []*vaa.VaaDoc + for i := range documents { + if len(documents[i].Vaas) != 1 { + r.logger.Warn("expected exactly 1 vaa document", + zap.Int("numVaas", len(documents[i].Vaas)), + zap.String("_id", documents[i].ID), + ) + } + vaas = append(vaas, &documents[i].Vaas[0]) + } + return &AddressOverview{Vaas: vaas}, nil +} diff --git a/api/handlers/address/service.go b/api/handlers/address/service.go new file mode 100644 index 00000000..45fa0874 --- /dev/null +++ b/api/handlers/address/service.go @@ -0,0 +1,47 @@ +package address + +import ( + "context" + + "github.com/wormhole-foundation/wormhole-explorer/api/internal/pagination" + "github.com/wormhole-foundation/wormhole-explorer/api/response" + "github.com/wormhole-foundation/wormhole-explorer/api/types" + "go.uber.org/zap" +) + +type Service struct { + repo *Repository + logger *zap.Logger +} + +func NewService(r *Repository, logger *zap.Logger) *Service { + + srv := Service{ + repo: r, + logger: logger.With(zap.String("module", "AddressService")), + } + + return &srv +} + +func (s *Service) GetAddressOverview( + ctx context.Context, + address *types.Address, + pagination *pagination.Pagination, +) (*response.Response[*AddressOverview], error) { + + response := &response.Response[*AddressOverview]{} + + p := GetAddressOverviewParams{ + Address: address, + Skip: pagination.Skip, + Limit: pagination.Limit, + } + overview, err := s.repo.GetAddressOverview(ctx, &p) + if err != nil { + return response, err + } + + response.Data = overview + return response, nil +} diff --git a/api/main.go b/api/main.go index c259c3b4..117525f7 100644 --- a/api/main.go +++ b/api/main.go @@ -18,6 +18,7 @@ import ( "github.com/improbable-eng/grpc-web/go/grpcweb" influxdb2 "github.com/influxdata/influxdb-client-go/v2" + "github.com/wormhole-foundation/wormhole-explorer/api/handlers/address" "github.com/wormhole-foundation/wormhole-explorer/api/handlers/governor" "github.com/wormhole-foundation/wormhole-explorer/api/handlers/heartbeats" "github.com/wormhole-foundation/wormhole-explorer/api/handlers/infrastructure" @@ -102,6 +103,7 @@ func main() { influxCli := newInfluxClient(cfg.Influx.URL, cfg.Influx.Token) // Set up repositories + addressRepo := address.NewRepository(db, rootLogger) vaaRepo := vaa.NewRepository(db, rootLogger) obsRepo := observations.NewRepository(db, rootLogger) governorRepo := governor.NewRepository(db, rootLogger) @@ -110,6 +112,7 @@ func main() { transactionsRepo := transactions.NewRepository(influxCli, cfg.Influx.Organization, cfg.Influx.Bucket, db, rootLogger) // Set up services + addressService := address.NewService(addressRepo, rootLogger) vaaService := vaa.NewService(vaaRepo, cacheGetFunc, rootLogger) obsService := observations.NewService(obsRepo, rootLogger) governorService := governor.NewService(governorRepo, rootLogger) @@ -136,7 +139,7 @@ func main() { // Set up route handlers app.Get("/swagger.json", GetSwagger) - wormscan.RegisterRoutes(app, rootLogger, vaaService, obsService, governorService, infrastructureService, transactionsService) + wormscan.RegisterRoutes(app, rootLogger, addressService, vaaService, obsService, governorService, infrastructureService, transactionsService) guardian.RegisterRoutes(cfg, app, rootLogger, vaaService, governorService, heartbeatsService) // Set up gRPC handlers diff --git a/api/middleware/extract_parameters.go b/api/middleware/extract_parameters.go index ca6e3e1c..0c7c186f 100644 --- a/api/middleware/extract_parameters.go +++ b/api/middleware/extract_parameters.go @@ -8,7 +8,6 @@ import ( "strings" "time" - solana "github.com/gagliardetto/solana-go" "github.com/gofiber/fiber/v2" "github.com/pkg/errors" "github.com/wormhole-foundation/wormhole-explorer/api/response" @@ -45,27 +44,14 @@ func ExtractEmitterAddr(c *fiber.Ctx, l *zap.Logger, chainIdHint *sdk.ChainID) ( emitterStr := c.Params("emitter") - // If the chain ID is Solana, attempt to parse the emitter as a Solana address. + // Decide whether to accept the Solana address format based on the context + var acceptSolanaFormat bool if chainIdHint != nil && *chainIdHint == sdk.ChainIDSolana { - - // If the address fails to parse, just fall back to the Wormhole format. - sig, err := solana.PublicKeyFromBase58(emitterStr) - if err == nil { - // This step is not expected to fail, since Solana and Wormhole addresses have the same size. - // However, if it does, we log the error. - emitter, err := types.BytesToAddress(sig[:]) - if err == nil { - return emitter, nil - } - l.Warn("failed to convert Solana address to Wormhole address", - zap.String("emitterAddress", emitterStr), - zap.Error(err), - ) - } + acceptSolanaFormat = true } - // Attempt to parse the address according to the Wormhole hex format. - emitter, err := types.StringToAddress(emitterStr) + // Attempt to parse the address + emitter, err := types.StringToAddress(emitterStr, acceptSolanaFormat) if err != nil { requestID := fmt.Sprintf("%v", c.Locals("requestid")) l.Error("failed to convert emitter to wormhole address", @@ -107,8 +93,8 @@ func ExtractGuardianAddress(c *fiber.Ctx, l *zap.Logger) (*types.Address, error) return nil, response.NewInvalidParamError(c, "MALFORMED GUARDIAN ADDR", nil) } - // validate the address - guardianAddress, err := types.StringToAddress(tmp) + // Attempt to parse the address + guardianAddress, err := types.StringToAddress(tmp, false /*acceptSolanaFormat*/) if err != nil { requestID := fmt.Sprintf("%v", c.Locals("requestid")) l.Error("failed to decode guardian address", @@ -188,6 +174,24 @@ func ExtractObservationHash(c *fiber.Ctx, l *zap.Logger) (string, error) { return hash, nil } +func ExtractAddress(c *fiber.Ctx, l *zap.Logger) (*types.Address, error) { + + val := c.Params("id") + + // Attempt to parse the address + addr, err := types.StringToAddress(val, true /*acceptSolanaFormat*/) + if err != nil { + requestID := fmt.Sprintf("%v", c.Locals("requestid")) + l.Error("failed to decode address", + zap.Error(err), + zap.String("requestID", requestID), + ) + return nil, response.NewInvalidParamError(c, "MALFORMED ADDR", errors.WithStack(err)) + } + + return addr, nil +} + // GetTxHash parses the `txHash` parameter from query params. func GetTxHash(c *fiber.Ctx, l *zap.Logger) (*types.TxHash, error) { diff --git a/api/routes/wormscan/address/controller.go b/api/routes/wormscan/address/controller.go new file mode 100644 index 00000000..d6eb1ec9 --- /dev/null +++ b/api/routes/wormscan/address/controller.go @@ -0,0 +1,60 @@ +package address + +import ( + "github.com/gofiber/fiber/v2" + "github.com/wormhole-foundation/wormhole-explorer/api/handlers/address" + "github.com/wormhole-foundation/wormhole-explorer/api/internal/errors" + "github.com/wormhole-foundation/wormhole-explorer/api/middleware" // required by swaggo + _ "github.com/wormhole-foundation/wormhole-explorer/api/response" // required by swaggo + "go.uber.org/zap" +) + +type Controller struct { + srv *address.Service + logger *zap.Logger +} + +func NewController(srv *address.Service, logger *zap.Logger) *Controller { + + c := Controller{ + srv: srv, + logger: logger.With(zap.String("module", "AddressController")), + } + + return &c +} + +// FindById godoc +// @Description Lookup an address +// @Tags Wormscan +// @ID find-address-by-id +// @Param address path string true "address" +// @Param page query integer false "Page number. Starts at 0." +// @Param pageSize query integer false "Number of elements per page." +// @Success 200 {object} response.Response[address.AddressOverview] +// @Failure 400 +// @Failure 404 +// @Failure 500 +// @Router /api/v1/address/{address} [get] +func (c *Controller) FindById(ctx *fiber.Ctx) error { + + address, err := middleware.ExtractAddress(ctx, c.logger) + if err != nil { + return err + } + + pagination, err := middleware.ExtractPagination(ctx) + if err != nil { + return err + } + + response, err := c.srv.GetAddressOverview(ctx.Context(), address, pagination) + if err != nil { + return err + } + if len(response.Data.Vaas) == 0 { + return errors.ErrNotFound + } + + return ctx.JSON(response) +} diff --git a/api/routes/wormscan/routes.go b/api/routes/wormscan/routes.go index 4e761946..4331937a 100644 --- a/api/routes/wormscan/routes.go +++ b/api/routes/wormscan/routes.go @@ -6,11 +6,13 @@ import ( "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cache" "github.com/gofiber/fiber/v2/middleware/cors" + addrsvc "github.com/wormhole-foundation/wormhole-explorer/api/handlers/address" govsvc "github.com/wormhole-foundation/wormhole-explorer/api/handlers/governor" infrasvc "github.com/wormhole-foundation/wormhole-explorer/api/handlers/infrastructure" obssvc "github.com/wormhole-foundation/wormhole-explorer/api/handlers/observations" trxsvc "github.com/wormhole-foundation/wormhole-explorer/api/handlers/transactions" vaasvc "github.com/wormhole-foundation/wormhole-explorer/api/handlers/vaa" + "github.com/wormhole-foundation/wormhole-explorer/api/routes/wormscan/address" "github.com/wormhole-foundation/wormhole-explorer/api/routes/wormscan/governor" "github.com/wormhole-foundation/wormhole-explorer/api/routes/wormscan/infrastructure" "github.com/wormhole-foundation/wormhole-explorer/api/routes/wormscan/observations" @@ -32,6 +34,7 @@ var cacheConfig = cache.Config{ func RegisterRoutes( app *fiber.App, rootLogger *zap.Logger, + addressService *addrsvc.Service, vaaService *vaasvc.Service, obsService *obssvc.Service, governorService *govsvc.Service, @@ -40,6 +43,7 @@ func RegisterRoutes( ) { // Set up controllers + addressCtrl := address.NewController(addressService, rootLogger) vaaCtrl := vaa.NewController(vaaService, rootLogger) observationsCtrl := observations.NewController(obsService, rootLogger) governorCtrl := governor.NewController(governorService, rootLogger) @@ -55,6 +59,9 @@ func RegisterRoutes( api.Get("/ready", infrastructureCtrl.ReadyCheck) api.Get("/version", infrastructureCtrl.Version) + // accounts resource + api.Get("/address/:id", addressCtrl.FindById) + // analytics api.Get("/last-txs", transactionCtrl.GetLastTransactions) api.Get("/x-chain-activity", transactionCtrl.GetChainActivity) diff --git a/api/rpc/handler.go b/api/rpc/handler.go index c3a55f93..52e46c78 100644 --- a/api/rpc/handler.go +++ b/api/rpc/handler.go @@ -221,18 +221,23 @@ func (h *Handler) GovernorGetEnqueuedVAAs(ctx context.Context, _ *publicrpcv1.Go // GovernorIsVAAEnqueued check if a vaa is enqueued. func (h *Handler) GovernorIsVAAEnqueued(ctx context.Context, request *publicrpcv1.GovernorIsVAAEnqueuedRequest) (*publicrpcv1.GovernorIsVAAEnqueuedResponse, error) { + if request.MessageId == nil { return nil, status.Error(codes.InvalidArgument, "Parameters are required") } + chainID := vaa.ChainID(request.MessageId.EmitterChain) - emitterAddress, err := types.StringToAddress(request.MessageId.EmitterAddress) + + emitterAddress, err := types.StringToAddress(request.MessageId.EmitterAddress, false /*acceptSolanaFormat*/) if err != nil { return nil, status.Error(codes.InvalidArgument, "Invalid emitter address") } + isEnqueued, err := h.govSrv.IsVaaEnqueued(ctx, chainID, emitterAddress, strconv.FormatUint(request.MessageId.Sequence, 10)) if err != nil { return nil, err } + return &publicrpcv1.GovernorIsVAAEnqueuedResponse{IsEnqueued: isEnqueued}, nil } diff --git a/api/types/address.go b/api/types/address.go index dcbb736b..4b911afb 100644 --- a/api/types/address.go +++ b/api/types/address.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/gagliardetto/solana-go" "github.com/wormhole-foundation/wormhole/sdk/vaa" ) @@ -25,8 +26,23 @@ func BytesToAddress(b []byte) (*Address, error) { } // StringToAddress converts a hex-encoded address string into an *Address. -func StringToAddress(s string) (*Address, error) { +func StringToAddress(s string, acceptSolanaFormat bool) (*Address, error) { + // Attempt to parse a Solana address (i.e.: 32 bytes encoded as base58). + // If it fails, fall back to parsing a regular Wormhole address. + if acceptSolanaFormat { + + sig, err := solana.PublicKeyFromBase58(s) + if err == nil { + // This step is not expected to fail, since Solana and Wormhole addresses have the same size. + emitter, err := BytesToAddress(sig[:]) + if err == nil { + return emitter, nil + } + } + } + + // Attempt to parse a regular Wormhole address (i.e.: 32 bytes encoded as hex). a, err := vaa.StringToAddress(s) if err != nil { return nil, err diff --git a/api/types/address_test.go b/api/types/address_test.go index 0a7e9891..9b87f5fd 100644 --- a/api/types/address_test.go +++ b/api/types/address_test.go @@ -6,9 +6,10 @@ import "testing" func Test_Address_ShortString(t *testing.T) { testCases := []struct { - Input string - Hex string - ShortHex string + Input string + AcceptSolanaFormat bool + Hex string + ShortHex string }{ { Input: "0x000000000000000000000000f890982f9310df57d00f659cf4fd87e65aded8d7", @@ -40,12 +41,31 @@ func Test_Address_ShortString(t *testing.T) { Hex: "ec7372995d5cc8732397fb0ad35c0121e0eaa90d26f828a534cab54391b3a4f5", ShortHex: "ec7372995d5cc8732397fb0ad35c0121e0eaa90d26f828a534cab54391b3a4f5", }, + { + Input: "31Sof5r1xi7dfcaz4x9Kuwm8J9ueAdDduMcme59sP8gc", + AcceptSolanaFormat: true, + Hex: "1dd48d0ee1fe7059b2866507b84f5f4259d7408c812e88bd6260a4914f7a2605", + ShortHex: "1dd48d0ee1fe7059b2866507b84f5f4259d7408c812e88bd6260a4914f7a2605", + }, + { + Input: "31Sof5r1xi7dfcaz4x9Kuwm8J9ueAdDduMcme59sP8gc", + AcceptSolanaFormat: false, + }, } for i := range testCases { tc := &testCases[i] - addr, err := StringToAddress(tc.Input) + addr, err := StringToAddress(tc.Input, tc.AcceptSolanaFormat /*acceptSolanaFormat*/) + + if tc.Hex == "" { + if err != nil { + continue + } else { + t.Fatalf("expected error, but got nil") + } + } + if err != nil { t.Fatalf("failed to parse address %s: %v", tc.Input, err) }